R standards (reference: August 2026)
Criteria verified as of August 2026. Re-verify on the web before committing to anything (§8).
1. Scope and triggers
Applies to all work in R: exploratory analysis, packages, reproducible reports, APIs, Shiny apps,
packaging and deployment. Triggers: .R, .Rmd, .qmd, DESCRIPTION, NAMESPACE, renv.lock,
.Rprofile, .lintr, app.R, plumber.R, tests/testthat/, src/*.cpp with Rcpp/cpp11.
The axis of this skill: R is an analysis language that ends up in production without having
been designed for it. A script that started on an analyst's laptop ends up serving an endpoint,
a scheduled report or a dashboard. This document sets out how that leap is made with a safety
net: reproducible environment, packaged code, tests, and an explicit trust boundary. R's
characteristic failure in production is not performance: it is that nobody can rebuild the
environment that produced the number.
Not applicable: see mlops-standards (the model life cycle is hers: model registry and
versioning, feature store, model serving and deployment, drift monitoring,
retraining, train/serve skew — how the R that trains or scores is written is ours),
data-engineering-standards (the data platform: ingestion, orchestration, idempotency, backfill,
Parquet, freshness SLA; the analysis code that consumes that platform is ours),
analytics-bi-standards (the dashboard as a decision artifact and its governance —
a Quarto report or a Shiny app that replaces a BI tool is a decision of hers;
the code of that report or app, ours), data-warehouse-modeling-standards (the shape of the
analytical model: grain, star, SCD), lakehouse-standards (table format and catalog behind
arrow/duckdb), sql-standards (the SQL that dbplyr generates or that you write in
DBI::dbGetQuery is subject to her criteria), python-standards (§7 sets when the correct
answer is Python), julia-standards (numerical performance; see §7), gpu-computing-standards (the
GPU as a resource that is provisioned, shared, monitored and paid for; the R code that uses it,
ours), llm-app-engineering-standards and rag-standards (AI application layer),
ai-governance-standards (model governance and regulatory compliance),
c-standards/cpp-standards (the native code on the other side of Rcpp/cpp11: memory, UB,
sanitizers, compiler flags; the boundary with R —SEXP, GC protection, packaging— is ours),
fortran-standards (the Fortran kernel on the other side of .Fortran() or of a package with
src/*.f90: bind(c), contiguity, index order and compiler flags are hers; the boundary from R
is ours),
cicd-standards (the pipeline that runs the gates in §4), kubernetes-standards (deployment of the
image), appsec-standards (agnostic threat modelling; here only R's sinks),
vulnerability-management-standards (triage and SLA of the finding; here only scanning the
project), secrets-management-standards,
observability-standards (the OTel/Prometheus pipeline; here only the instrumentation in the code),
api-design-standards (the contract of a Plumber API: resources, codes, pagination,
versioning).
2. Default toolchain
Verify the latest version on the web before committing to it in a real project (§8).
| Component |
Choice |
Verified as of Aug 2026 |
Why |
| Runtime |
R of the current stable series |
4.6.1 (2026-06-24); 4.6.0 came out 2026-04-24 |
Minor once a year, in spring; patches when needed |
| "Conservative" version |
Last patch of the previous series |
4.5.3 (2026-03-11) |
R Core publishes a final patch of the previous series shortly before the next x.y.0. R has no LTS: the closest thing is that last patch, never >1 year old |
| Environment/reproducibility |
renv |
1.2.3 (2026-05-16), MIT |
Non-negotiable: renv.lock versioned or the project is not reproducible |
| Repository |
Posit Package Manager (P3M) with a date snapshot |
https://packagemanager.posit.co/cran/YYYY-MM-DD |
Daily snapshots (working days) since 2017-10-10; Linux binaries. Plain CRAN does not give temporal reproducibility |
| Bioconductor |
Only if the domain requires it |
3.23 (2026-04-29) ↔ R 4.6 |
Cycle coupled to R: 2 releases/year; the Bioc version fixes the R version, not the other way round |
| Data manipulation |
dplyr/tidyverse or data.table — choose one per project |
dplyr 1.2.1 (2026-04-03), MIT; data.table 1.18.4 (2026-05-06), MPL-2.0 |
See criteria below |
| Style |
styler |
1.11.0 (2025-10-13), MIT |
The GitHub repo has published no release since 2024 but CRAN has: it is not abandoned, it publishes via CRAN |
| Lint (CI gate) |
lintr |
3.4.0 (2026-07-16) |
Config in a versioned .lintr |
| Tests |
testthat 3rd edition |
3.3.2 (2026-01-12) |
Enabled explicitly: Config/testthat/edition: 3 in DESCRIPTION. It is not the default |
| Documentation |
roxygen2 |
8.0.0 (2026-05-01) |
Recent major: review breaking changes before upgrading |
| Reports |
Quarto |
quarto-cli 1.11.1 (2026-07-28) |
Replaces R Markdown in a new project; Rmd only in legacy |
| HTTP API |
plumber |
1.3.3 (2026-01-28), MIT |
— |
| Interactive app |
shiny (R package) |
MIT |
The package is MIT; the hosting is not (see §5/§7) |
| Data that does not fit |
arrow + duckdb |
— |
It pushes the work outside R's RAM before rewriting in another language |
| Native |
cpp11 in new code; Rcpp in legacy |
— |
cpp11 does not use heavy C++ macros and compiles faster; Rcpp is still the majority ecosystem |
| Container |
Rocker images (rocker/r-ver:<version>) |
— |
r-ver pins the R version and the repository snapshot |
Binary compatibility trap (verified): R 4.6.0 changed headers and the version of the graphics
engine API (16 → 17); already installed compiled packages stopped loading (reported cases:
data.table, RSQLite). Rule: when upgrading an R minor, reinstall the whole library of compiled
packages, do not reuse the previous .libPaths(). renv::rebuild() or a new image.
tidyverse vs data.table vs base R criteria (it is a criterion, not a side):
- tidyverse when the code is going to be read and maintained by analysts, when the project is
already tidyverse, and when the volume fits comfortably in RAM. Cost: a large dependency
tree and an API that evolves (deprecations with a cycle, but it evolves).
- data.table when performance or memory rule (aggregations over millions of rows,
updates by reference), or when you want a single dependency. Its API is
extraordinarily stable — a real argument for long-lived code. Cost: dense syntax.
- base R for packages with minimal
Imports and for infrastructure utilities. Cost:
verbosity and traps (§3).
- Forbidden to mix the three styles in the same file. A package may have different
modules with different styles; a function, no.
3. Structure and conventions
Loose script vs package — the criterion that defines this skill. An analysis stops being a
script and becomes a package as soon as any of these happens: (a) a function is used from two
files, (b) somebody else is going to run it, (c) the result feeds a recurring decision, (d) it has
to be tested. Turning it into a package is what gives you, for free, everything an analysis in
production needs: a namespace, dependencies declared in DESCRIPTION, documentation with
roxygen2, tests with testthat, and R CMD check as a gate. You do not need to publish on CRAN
to package.
project/
DESCRIPTION # deps declared: Imports (real use), Suggests (optional), Depends almost never
NAMESPACE # generated by roxygen2 — never by hand
renv.lock # ALWAYS versioned
.Rprofile # activates renv; no business logic
R/ # functions; no code with effects on load
tests/testthat/
inst/ # entry-point scripts, templates
analysis/ or vignettes/ # Quarto/Rmd that CALL R/, not that contain the logic
src/ # cpp11/Rcpp if applicable
library() and setwd() forbidden inside R/: in a package, dependencies are declared in
DESCRIPTION and used with pkg::fun() or @importFrom. Paths with here::here() or
system.file().
- No side effects on load: no
library(), no global options(), no DB connections nor
file reads in the body of R/*.R. Anything needing state goes in .onLoad/an explicit function.
- A Quarto/Rmd report is not the place for the logic:
.qmd orchestrates and narrates; the
functions live in R/ and are tested. A report with 300 lines of embedded transformation is debt
by default.
- Names:
snake_case verbal functions; no df, df2, tmp; no . as a separator (it clashes with
S3 dispatch). No catch-all utils.R.
- S3 objects by default; S4 only if the domain already requires it (Bioconductor) or dispatch on
multiple arguments is needed; R5/RC practically never. S7 exists but verify its maturity (§8)
before committing to it in a new project.
options(stringsAsFactors) no longer exists as a trap: since R 4.0.0 the default is FALSE. But
legacy code that assumed factors still exists — when touching pre-4.0 code, check whether it
depended on the coercion. Factors are created explicitly, with levels fixed by hand
when the order matters; a factor with levels inferred from today's data breaks tomorrow.
Language traps that are first-class bugs (treat them as such, not as folklore):
- Silent vector recycling:
x + y with different lengths does not always warn. Validate
lengths at the edges; in critical arithmetic, stopifnot(length(x) == length(y)).
NA propagates: sum(x) without na.rm gives NA; if (NA) is an error; x == NA is NA,
use is.na(). Decide explicitly per column what NA means — never na.rm = TRUE by
reflex, because it changes the semantics of the result without leaving a trace.
[ on a data.frame with a single result collapses to a vector: use drop = FALSE or tibbles.
sapply() returns different types depending on the data: in production code, vapply() with
an explicit FUN.VALUE or purrr's typed variants (map_dbl, map_chr).
- Comparing floats with
==: all.equal() / a tolerance.
- Lazy evaluation of arguments:
force() when you capture arguments in closures.
Non-standard evaluation (NSE). Tidy evaluation is what makes dplyr readable and what
breaks defensive programming: inside filter(data, x > 1), x is not a variable of the
environment, it is a column, and if the column does not exist R may silently pick up an object from
the environment with that name. Hard rules:
- In package functions, always reference columns with the
.data$col pronoun (or
.data[[var]]) — that way the failure is "non-existent column", not "it took your global
variable".
- To pass column names from your function's arguments:
{{ arg }} (embracing); for
several, ... passed through as is. !!sym(chr) only if the name arrives as a string.
aes_string(), filter_(), mutate_() and the rest of the _ variants are retired: they
are not used.
- Declare
.data (and the column names you use in NSE) so that R CMD check does not generate the
classic "no visible binding for global variable" — with utils::globalVariables() as a last
resort, not as the norm.
Errors and conditions:
stop()/warning() with an actionable message; in new packages, rlang::abort() with a
condition class so that the caller can catch by class (tryCatch(err_empty_data = ...))
instead of by grepl over the message.
- Failing is correct; returning a half-finished result is not. Forbidden:
try(..., silent = TRUE) without inspecting the result, and blanket suppressWarnings() over a whole block.
on.exit(add = TRUE) to release connections, files and modified options() — R's defer.
warning() does not interrupt: nothing critical is signalled with warning.
4. Quality: formatting, linting, tests, documentation
- Formatting:
styler (tidyverse style guide) applied to the whole repo; a single
configuration.
- Lint:
lintr with a versioned .lintr, run in CI as a gate. Minimum: a fixed line length,
object_name_linter, seq_linter (1:n is a bug when n == 0 → seq_len(n)),
undesirable_function_linter (vetoes attach, setwd, sapply, library in R/),
T/F forbidden (they are reassignable variables; use TRUE/FALSE).
- Tests with
testthat 3rd edition (Config/testthat/edition: 3):
- One test file per file in
R/; expect_* with AAA and one failure reason per test.
- Cover the happy path and the edges: empty vector,
NA, NULL, missing column, unexpected
type, factor with an unseen level, date in another time zone, duplicates.
- Snapshot tests (
expect_snapshot) for error messages and formatted outputs; review the
_snaps/ in the PR as code.
- Randomness: explicit
set.seed() in the test, or withr::local_seed(). No tests that
depend on the environment's global RNGkind.
- No public network and no writing to the user's directory:
withr::local_tempdir().
- Every fixed bug leaves a regression test. Flaky = fixed or deleted.
- Documentation:
roxygen2 for every exported function (@param, @return, runnable
@examples). An @export without documentation is a review failure. pkgdown if the package is
consumed by third parties.
- CI gates (they block the merge, cheapest first):
renv::status() — fails if the lock is out of sync.
styler in check mode + lintr::lint_package().
R CMD check --as-cran (or devtools::check()): zero ERROR, zero WARNING; NOTEs are
justified in writing or fixed.
testthat with coverage (covr); an agreed threshold — coverage is a signal, not a goal.
- Dependency audit (§5) and building the image.
- CI matrix: the R version pinned in production, plus the previous one if you support external
users. Pin the P3M snapshot in CI so that a CRAN release does not break yesterday's build.
5. Stack security
readRDS() / load() / unserialize() over untrusted input is code execution. A serialised R
object can carry environments, promises and classes with methods that run when printed or when
restored. Rule: never deserialise an .rds/.RData that comes from outside your
trust boundary; for exchange use pure data formats (Parquet, CSV, JSON) validated when
read. load() also pollutes the global environment — forbidden in package code.
eval(parse(text = ...)) over user input: FORBIDDEN. It is R's eval and it is the classic
Shiny vulnerability. Nor parse(), str2lang(), source() of paths built
with input, nor do.call(name_as_text, ...) without an allowlist.
- Shiny exposes R to the Internet. Every
input$* is hostile input:
- Validate on the server, not in the UI: the constraint of a
selectInput does not exist in
the protocol, a client can send any value. validate()/req() are not security
validation.
- Never use
input$* to build SQL, file paths, object names or commands
(system(), system2()). An allowlist of permitted values, not a blacklist.
fileInput: a size limit (shiny.maxRequestSize), type verified by content, and the
file processed in a temporary location — never served back and never deserialised.
- HTML:
HTML(), tags$script, htmltools::HTML and renderText with escape = FALSE are XSS
if input gets in. By default, escaped text.
- Authentication: open source Shiny Server ships no authentication — it is solved in front
(reverse proxy with OIDC) or with a product that includes it. Do not implement login in the
server() itself.
- SQL:
DBI::dbGetQuery with params = list(...) or glue::glue_sql(); paste0() of input
into a query is an absolute veto. With dbplyr, review the generated SQL (show_query()) — its
criteria belong to sql-standards.
install.packages() at run time: FORBIDDEN in production. Installing from the
started container or from an app's server() means the artifact is not immutable,
that the build depends on the network and that the version running today is not the one that was
tested. All dependencies are installed at build time, from a pinned snapshot. The same for
remotes::install_github() outside a Dockerfile with the commit pinned by SHA.
- CRAN does not audit security. CRAN checks that the package works, not that it is secure nor
that its maintainer is still alive. Before adding a dependency: recent maintenance, number of
maintainers, licence, and whether it drags in a
SystemRequirements that widens the container's
surface. A package can execute arbitrary code on installation (configure, .onLoad).
- Dependency audit: the R ecosystem has no mature equivalent to
pip-audit.
What there is: oysteR (CRAN 0.1.4, 2025-10-09, Apache-2.0), which queries Sonatype OSS
Index; the project itself states that it is not supported by Sonatype (a community
contribution) and that heavy use runs into rate limiting. Use it as a signal
(audit_renv_lock() in CI, non-blocking at first), complemented with OSV/GitHub Advisories over
the renv.lock, and assume incomplete coverage: the absence of findings in R is not evidence
of the absence of vulnerabilities. Check in §8 whether something better has appeared.
- Secrets: never in
.Rprofile, a versioned .Renviron, renv.lock, code or reports. Env
vars or a manager; local .Renviron in .gitignore. Careful with .RData files saved on exit:
disable automatic session saving (--no-save, --no-restore in any non-interactive
run) — an .RData with credentials in the repo is a classic incident.
- Reports: a rendered Quarto/Rmd embeds whatever you print. Check that no connection
strings, tokens or personal data come out in the outputs or in the warning messages.
- Containers: an image based on
rocker/r-ver with the R version and snapshot pinned, non-root,
multi-stage. The real problem with R in containers is the system dependencies: many
packages compile against OS libraries (libcurl, libxml2, libssl, libgdal, libproj,
libgit2). Install them explicitly in the Dockerfile (P3M exposes the SystemRequirements);
do not trust that "they were in the base image". And do not leave them in the final image if they
were only needed to compile.
6. Performance and operability
- Order of attack, in this order and no other: (1) measure (
profvis, bench::mark) — never
optimise by intuition; (2) vectorise and eliminate object growth in loops (x <- c(x, i) is
quadratic: preallocate or use vapply); (3) data.table for heavy aggregation/joins; (4) push
the computation to arrow/duckdb or to the database when the data does not fit in RAM; (5)
cpp11/Rcpp only for the loop that genuinely cannot be vectorised, and only after 1-4.
- R copies on modify and the memory peak is the problem, not the CPU.
data.table modifies by
reference (:=) — powerful and a source of bugs if the object is shared: document when a
function mutates its argument, or return an explicit copy.
- Parallelism:
future/furrr or parallel. multicore (fork) is not safe on a Shiny/
Plumber server nor on Windows: use multisession or external processes. Never launch more
workers than the cores assigned to the container — R does not see the cgroup limit on its own.
- Plumber: it is single-threaded. One slow request blocks everybody. Scale with multiple
processes behind a load balancer, explicit timeouts on every outbound call
(
httr2::req_timeout), and heavy work outside the request. /healthz and /readyz endpoints;
structured logging with a correlation id.
- Shiny in production: each session is state on the server and one R process serves N sessions in
a single thread. Consequences: any long computation in
server() freezes all the users
of that process (move it to future/a job queue or precompute); session state does not survive
the process crashing nor migrate between replicas (session affinity mandatory on the load
balancer, and an app that "restarts on its own" is a user losing their work); badly isolated
reactivity = data leaks between sessions if you put state in the global environment. Size by
concurrent sessions and RAM per session, not by requests/second. Large shared read-only
objects: load them once outside server() (they are shared across sessions of the same process),
never per-user data.
- DB connections: a pool (
pool) with limits; one connection per Shiny session exhausts itself.
Always close with on.exit.
- Scheduled reports: idempotent, with explicit parameters and versioned output. A report that
fails must fail loudly, not publish yesterday's version.
- Seed and versions in the artifact: every report/model publishes the R version, the
renv.lock (or
its hash) and the seed. Without that, a number is not reproducible even if the code is in git.
7. Long-term sustainability
- Cadence: an R minor once a year (spring) — plan it as an event, with reinstallation of
compiled packages and a full run of the suite. R patches, apply them. P3M snapshot:
advance it deliberately (quarterly, with the suite green) instead of floating or freezing it
for years; a 3-year-old snapshot is as dangerous as having none, because the day it has to be
moved the jump is impossible.
- Bioconductor drags the R version along: if you depend on it, your calendar is theirs (two
releases a year), not the other way round.
- Deprecations: tidyverse warns with long cycles but it warns;
lifecycle badges and
DeprecationWarning are treated as debt with an issue, they are not silenced. data.table
hardly ever breaks its API — that is its value.
- A dependency with no release in >2 years or with a single maintainer is reviewed; if it is on the
critical path of production, the function you use is vendored or replaced.
- Debt from an analysis that becomes a service: when a script starts serving traffic,
it is rewritten as a package with tests before exposing it, not afterwards. "We'll wrap it in
Plumber and that's it" is the most expensive debt in this ecosystem: nobody knows what inputs it
accepts, there are no tests, the state lives in the global environment and the first incident is
at 3 in the morning. If there is no budget for the rewrite, there is no budget for the service:
publish it as a scheduled report.
When NOT to choose R (honesty first):
- ❌ R as a general-purpose application language (transactional backend, system CLI,
microservice with business logic): use Python, Go or TypeScript.
- ❌ R for pipeline or infrastructure orchestration: that is
data-engineering-standards.
- ❌ R because "the analyst knows it": if the artifact is a service with an SLA and nobody on the
team maintains R in production, the correct choice is to port it.
- ✅ R is the right answer against Python in: serious statistical modelling (mixed models,
survival, time series, Bayesian inference, experimental design), biostatistics and
Bioconductor, publication-quality graphics (
ggplot2), and reproducible reports where the
narrative and the computation go together. There R's ecosystem of specialised packages has no
equivalent.
- ✅ Python (see
python-standards) when the work is engineering around the analysis:
serving, integration, production ML, orchestration, or when the team that will maintain it is an
engineering one. Pragmatic boundary: if the result is a number or a report, R; if the result is a
system, Python.
- ✅ Julia (see
julia-standards) only when the bottleneck is a numerical loop that does not
vectorise. The choice is almost never "R or Julia": R is statistics and communication of results,
Julia is numerical performance. They share the niche of "a scientific language that is not
Python" and little else.
List of prohibitions (veto):
- ❌ A project in production without a versioned
renv.lock, or with a CRAN repository without a
pinned snapshot.
- ❌
install.packages() / remotes::install_github() at run time in production.
- ❌
readRDS()/load()/unserialize() over untrusted input. load() inside a package.
- ❌
eval(parse(text = ...)) with user input. source() of a path built with input.
- ❌
setwd(), attach(), library() inside R/; rm(list = ls()) as a "restart".
- ❌ Relying on the session's saved
.RData: always run with --no-save --no-restore.
- ❌ SQL via
paste0. Shiny input into system(), paths or object names without an allowlist.
- ❌
T/F instead of TRUE/FALSE. 1:n where n may be 0. sapply() in production code.
- ❌
na.rm = TRUE by reflex, without deciding what NA means in that column.
- ❌ Blanket
suppressWarnings()/try(silent = TRUE); catching and swallowing with no log and no
re-raise.
- ❌ Retired NSE variants (
aes_string, *_ with an underscore) in new code.
- ❌ Business logic inside a
.qmd/.Rmd instead of in R/ with tests.
- ❌
R CMD check with a WARNING and "we'll look at it later"; a NOTE with no written justification.
- ❌ User state in the global environment of a Shiny app (leakage between sessions).
- ❌ Upgrading an R minor while reusing the previous library of compiled packages (see §2).
- ❌ A long synchronous computation inside Shiny's
server() or a Plumber endpoint.
8. Mandatory web verification
Before committing to versions or decisions, verify online (WebSearch/WebFetch; for versions, the
Atom feeds of GitHub Releases and the CRAN pages — not the summariser over GitHub's HTML):
- The latest stable R and the last patch of the previous series (
cran.r-project.org/src/base/R-4/,
developer.r-project.org). As of Aug 2026: 4.6.1 (2026-06-24) and 4.5.3 (2026-03-11).
R has no declared LTS — do not assert it.
- The Bioconductor version and the R version it is coupled to
(
bioconductor.org/about/release-announcements/). As of Aug 2026: 3.23 ↔ R 4.6.
- The state of Posit Package Manager (snapshot URL, binary coverage per distro, availability
of the public service and its terms of use). Public snapshots exist since 2017-10-10 and only
on working days.
- Versions and licences of
renv, styler, lintr, testthat, roxygen2, plumber,
data.table (MPL-2.0, not MIT), shiny. Check CRAN as well as GitHub: styler has
published no release on GitHub since 2024 but its latest CRAN version is 1.11.0 (2025-10-13) —
a quiet repo ≠ an abandoned package.
- Posit's commercial model (an expensive and changing figure): the
shiny package is MIT and
open source Shiny Server is AGPL-3.0, but Shiny Server Pro was discontinued on 2026-03-31
and Posit directs you to Posit Connect, commercial and licensed by active users;
anonymous public access to interactive content is a paid entitlement (Enhanced/Advanced
licence). Verify before designing hosting: prices not published, alternatives ShinyProxy (open
source) and Connect Cloud/shinyapps.io. Declared discrepancy: I have found no official
Posit statement putting open source Shiny Server into maintenance mode, but its last release
on GitHub is from 2024-09-30; treat its future as an open risk, not as a fact.
- Vulnerability auditing: the state of
oysteR and whether a maintained alternative already exists
(post-check: as of Aug 2026 there is none). Declared gap, not verified as of Aug 2026: the real coverage
of CRAN in OSV/OSS Index (what percentage of packages have advisories) I have not been able to
quantify — do not assert it.
- Declared gap, not verified as of Aug 2026: the maturity of S7 as the default object system and
whether it has already entered base R; and the state of
cpp11 versus Rcpp after the header
change in R 4.6.0.
- Breaking changes in
roxygen2 8.x and in R's 4.6 series (graphics API 16→17, headers) from the
official NEWS before any upgrade — never from third-party blogs without cross-checking.
If the web contradicts this document, the web wins — flag the discrepancy.
1---2name: r-standards3description: Use when writing, reviewing or productionizing R code - .R/.Rmd/.qmd/.Rproj files, DESCRIPTION, NAMESPACE, renv.lock, .Rprofile, .lintr, _pkgdown.yml, testthat tests, roxygen2 blocks, tidyverse/dplyr/ggplot2 or data.table pipelines, non-standard evaluation with {{ }} and .data, CRAN/Bioconductor/Posit Package Manager repositories, Shiny apps (app.R, server.R, ui.R), Plumber APIs (plumber.R), Quarto or R Markdown reports, Rcpp/cpp11 native code, or rocker/r-base container images.4---56# R standards (reference: August 2026)78Criteria verified as of **August 2026**. Re-verify on the web before committing to anything (§8).910## 1. Scope and triggers1112Applies to all work in R: exploratory analysis, packages, reproducible reports, APIs, Shiny apps,13packaging and deployment. Triggers: `.R`, `.Rmd`, `.qmd`, `DESCRIPTION`, `NAMESPACE`, `renv.lock`,14`.Rprofile`, `.lintr`, `app.R`, `plumber.R`, `tests/testthat/`, `src/*.cpp` with Rcpp/cpp11.1516**The axis of this skill**: R is an *analysis* language that ends up in production without having17been designed for it. A script that started on an analyst's laptop ends up serving an endpoint,18a scheduled report or a dashboard. This document sets out how that leap is made **with a safety19net**: reproducible environment, packaged code, tests, and an explicit trust boundary. R's20characteristic failure in production is not performance: it is that **nobody can rebuild the21environment that produced the number**.2223**Not applicable**: see `mlops-standards` (**the model life cycle is hers**: model registry and24versioning, *feature store*, model serving and deployment, drift monitoring,25retraining, *train/serve skew* — **how the R that trains or scores is written is ours**),26`data-engineering-standards` (the data platform: ingestion, orchestration, idempotency, *backfill*,27Parquet, freshness SLA; the analysis code that consumes that platform is ours),28`analytics-bi-standards` (the dashboard as a decision artifact and its governance —29**a Quarto report or a Shiny app that replaces a BI tool is a decision of hers**;30the code of that report or app, ours), `data-warehouse-modeling-standards` (the shape of the31analytical model: grain, star, SCD), `lakehouse-standards` (table format and catalog behind32`arrow`/`duckdb`), `sql-standards` (**the SQL that `dbplyr` generates or that you write in33`DBI::dbGetQuery` is subject to her criteria**), `python-standards` (§7 sets when the correct34answer is Python), `julia-standards` (numerical performance; see §7), `gpu-computing-standards` (the35GPU as a resource that is provisioned, shared, monitored and paid for; the R code that uses it,36ours), `llm-app-engineering-standards` and `rag-standards` (AI application layer),37`ai-governance-standards` (model governance and regulatory compliance),38`c-standards`/`cpp-standards` (**the native code on the other side of `Rcpp`/`cpp11`**: memory, UB,39sanitizers, compiler flags; the boundary with R —`SEXP`, GC protection, packaging— is ours),40`fortran-standards` (**the Fortran kernel on the other side of `.Fortran()` or of a package with41`src/*.f90`**: `bind(c)`, contiguity, index order and compiler flags are hers; the boundary from R42is ours),43`cicd-standards` (the pipeline that runs the gates in §4), `kubernetes-standards` (deployment of the44image), `appsec-standards` (agnostic threat modelling; here only R's *sinks*),45`vulnerability-management-standards` (triage and SLA of the finding; here only scanning the46project), `secrets-management-standards`,47`observability-standards` (the OTel/Prometheus pipeline; here only the instrumentation in the code),48`api-design-standards` (the **contract** of a Plumber API: resources, codes, pagination,49versioning).5051## 2. Default toolchain5253> Verify the latest version on the web before committing to it in a real project (§8).5455| Component | Choice | Verified as of Aug 2026 | Why |56|---|---|---|---|57| Runtime | **R** of the current stable series | 4.6.1 (2026-06-24); 4.6.0 came out 2026-04-24 | Minor **once a year, in spring**; patches when needed |58| "Conservative" version | Last patch of the previous series | 4.5.3 (2026-03-11) | R Core publishes a final patch of the previous series shortly before the next x.y.0. **R has no LTS**: the closest thing is that last patch, never >1 year old |59| Environment/reproducibility | **`renv`** | 1.2.3 (2026-05-16), MIT | **Non-negotiable**: `renv.lock` versioned or the project is not reproducible |60| Repository | **Posit Package Manager (P3M)** with a date snapshot | `https://packagemanager.posit.co/cran/YYYY-MM-DD` | Daily snapshots (working days) since 2017-10-10; Linux binaries. Plain CRAN does not give temporal reproducibility |61| Bioconductor | Only if the domain requires it | 3.23 (2026-04-29) ↔ R 4.6 | **Cycle coupled to R**: 2 releases/year; the Bioc version fixes the R version, not the other way round |62| Data manipulation | `dplyr`/`tidyverse` **or** `data.table` — choose one per project | dplyr 1.2.1 (2026-04-03), MIT; data.table 1.18.4 (2026-05-06), MPL-2.0 | See criteria below |63| Style | **`styler`** | 1.11.0 (2025-10-13), MIT | The GitHub repo has published no *release* since 2024 but **CRAN has**: it is not abandoned, it publishes via CRAN |64| Lint (CI gate) | **`lintr`** | 3.4.0 (2026-07-16) | Config in a versioned `.lintr` |65| Tests | **`testthat` 3rd edition** | 3.3.2 (2026-01-12) | Enabled **explicitly**: `Config/testthat/edition: 3` in `DESCRIPTION`. It is not the default |66| Documentation | **`roxygen2`** | 8.0.0 (2026-05-01) | Recent major: review breaking changes before upgrading |67| Reports | **Quarto** | quarto-cli 1.11.1 (2026-07-28) | Replaces R Markdown in a new project; Rmd only in legacy |68| HTTP API | **`plumber`** | 1.3.3 (2026-01-28), MIT | — |69| Interactive app | **`shiny`** (R package) | MIT | The **package** is MIT; the hosting is not (see §5/§7) |70| Data that does not fit | `arrow` + `duckdb` | — | It pushes the work outside R's RAM before rewriting in another language |71| Native | `cpp11` in new code; `Rcpp` in legacy | — | `cpp11` does not use heavy C++ macros and compiles faster; `Rcpp` is still the majority ecosystem |72| Container | **Rocker** images (`rocker/r-ver:<version>`) | — | `r-ver` pins the R version **and** the repository snapshot |7374**Binary compatibility trap (verified)**: R 4.6.0 changed headers and the version of the graphics75engine API (16 → 17); already installed **compiled** packages stopped loading (reported cases:76`data.table`, `RSQLite`). Rule: when upgrading an R minor, **reinstall the whole library of compiled77packages**, do not reuse the previous `.libPaths()`. `renv::rebuild()` or a new image.7879**tidyverse vs data.table vs base R criteria** (it is a criterion, not a side):80- **tidyverse** when the code is going to be read and maintained by analysts, when the project is81 already tidyverse, and when the volume fits comfortably in RAM. Cost: a large dependency82 tree and an API that evolves (deprecations with a cycle, but it evolves).83- **data.table** when performance or memory rule (aggregations over millions of rows,84 *updates by reference*), or when you want **a single dependency**. Its API is85 extraordinarily stable — a real argument for long-lived code. Cost: dense syntax.86- **base R** for packages with minimal `Imports` and for infrastructure utilities. Cost:87 verbosity and traps (§3).88- Forbidden to **mix the three styles in the same file**. A package may have different89 modules with different styles; a function, no.9091## 3. Structure and conventions9293**Loose script vs package — the criterion that defines this skill.** An analysis stops being a94script and becomes a **package** as soon as any of these happens: (a) a function is used from two95files, (b) somebody else is going to run it, (c) the result feeds a recurring decision, (d) it has96to be tested. Turning it into a package is what gives you, for free, everything an analysis in97production needs: a namespace, dependencies declared in `DESCRIPTION`, documentation with98`roxygen2`, tests with `testthat`, and `R CMD check` as a gate. **You do not need to publish on CRAN99to package.**100101```102project/103 DESCRIPTION # deps declared: Imports (real use), Suggests (optional), Depends almost never104 NAMESPACE # generated by roxygen2 — never by hand105 renv.lock # ALWAYS versioned106 .Rprofile # activates renv; no business logic107 R/ # functions; no code with effects on load108 tests/testthat/109 inst/ # entry-point scripts, templates110 analysis/ or vignettes/ # Quarto/Rmd that CALL R/, not that contain the logic111 src/ # cpp11/Rcpp if applicable112```113114- `library()` and `setwd()` **forbidden inside `R/`**: in a package, dependencies are declared in115 `DESCRIPTION` and used with `pkg::fun()` or `@importFrom`. Paths with `here::here()` or116 `system.file()`.117- No side effects on load: no `library()`, no global `options()`, no DB connections nor118 file reads in the body of `R/*.R`. Anything needing state goes in `.onLoad`/an explicit function.119- A Quarto/Rmd report **is not the place for the logic**: `.qmd` orchestrates and narrates; the120 functions live in `R/` and are tested. A report with 300 lines of embedded transformation is debt121 by default.122- Names: `snake_case` verbal functions; no `df`, `df2`, `tmp`; no `.` as a separator (it clashes with123 S3 dispatch). No catch-all `utils.R`.124- **S3 objects by default**; S4 only if the domain already requires it (Bioconductor) or dispatch on125 multiple arguments is needed; R5/RC practically never. **S7** exists but verify its maturity (§8)126 before committing to it in a new project.127- `options(stringsAsFactors)` no longer exists as a trap: since R 4.0.0 the default is `FALSE`. But128 legacy code that **assumed** factors still exists — when touching pre-4.0 code, check whether it129 depended on the coercion. Factors are created **explicitly**, with `levels` fixed by hand130 when the order matters; a factor with levels inferred from today's data breaks tomorrow.131132**Language traps that are first-class bugs** (treat them as such, not as folklore):133- **Silent vector recycling**: `x + y` with different lengths does not always warn. Validate134 lengths at the edges; in critical arithmetic, `stopifnot(length(x) == length(y))`.135- **`NA` propagates**: `sum(x)` without `na.rm` gives `NA`; `if (NA)` is an error; `x == NA` is `NA`,136 use `is.na()`. Decide **explicitly** per column what `NA` means — never `na.rm = TRUE` by137 reflex, because it changes the semantics of the result without leaving a trace.138- `[` on a `data.frame` with a single result collapses to a vector: use `drop = FALSE` or tibbles.139- `sapply()` returns different types depending on the data: in production code, `vapply()` with140 an explicit `FUN.VALUE` or `purrr`'s typed variants (`map_dbl`, `map_chr`).141- Comparing floats with `==`: `all.equal()` / a tolerance.142- Lazy evaluation of arguments: `force()` when you capture arguments in closures.143144**Non-standard evaluation (NSE)**. *Tidy* evaluation is what makes `dplyr` readable **and what145breaks defensive programming**: inside `filter(data, x > 1)`, `x` is not a variable of the146environment, it is a column, and if the column does not exist R may silently pick up an object from147the environment with that name. Hard rules:148- In **package functions**, always reference columns with the `.data$col` pronoun (or149 `.data[[var]]`) — that way the failure is "non-existent column", not "it took your global150 variable".151- To pass column names from your function's arguments: `{{ arg }}` (*embracing*); for152 several, `...` passed through as is. `!!sym(chr)` only if the name arrives as a string.153- `aes_string()`, `filter_()`, `mutate_()` and the rest of the `_` variants are **retired**: they154 are not used.155- Declare `.data` (and the column names you use in NSE) so that `R CMD check` does not generate the156 classic "no visible binding for global variable" — with `utils::globalVariables()` as a last157 resort, not as the norm.158159**Errors and conditions**:160- `stop()`/`warning()` with an actionable message; in new packages, `rlang::abort()` with a161 **condition class** so that the caller can catch by class (`tryCatch(err_empty_data = ...)`)162 instead of by `grepl` over the message.163- **Failing is correct; returning a half-finished result is not.** Forbidden: `try(..., silent =164 TRUE)` without inspecting the result, and blanket `suppressWarnings()` over a whole block.165- `on.exit(add = TRUE)` to release connections, files and modified `options()` — R's `defer`.166- `warning()` does not interrupt: nothing critical is signalled with `warning`.167168## 4. Quality: formatting, linting, tests, documentation169170- **Formatting**: `styler` (tidyverse style guide) applied to the whole repo; a single171 configuration.172- **Lint**: `lintr` with a versioned `.lintr`, run in CI as a gate. Minimum: a fixed line length,173 `object_name_linter`, `seq_linter` (`1:n` is a bug when `n == 0` → `seq_len(n)`),174 `undesirable_function_linter` (vetoes `attach`, `setwd`, `sapply`, `library` in `R/`),175 `T`/`F` forbidden (they are reassignable variables; use `TRUE`/`FALSE`).176- **Tests with `testthat` 3rd edition** (`Config/testthat/edition: 3`):177 - One test file per file in `R/`; `expect_*` with AAA and one failure reason per test.178 - Cover the happy path **and the edges**: empty vector, `NA`, `NULL`, missing column, unexpected179 type, factor with an unseen level, date in another time zone, duplicates.180 - Snapshot tests (`expect_snapshot`) for error messages and formatted outputs; review the181 `_snaps/` in the PR as code.182 - Randomness: explicit `set.seed()` in the test, or `withr::local_seed()`. No tests that183 depend on the environment's global `RNGkind`.184 - No public network and no writing to the user's directory: `withr::local_tempdir()`.185 - Every fixed bug leaves a regression test. Flaky = fixed or deleted.186- **Documentation**: `roxygen2` for every exported function (`@param`, `@return`, runnable187 `@examples`). An `@export` without documentation is a review failure. `pkgdown` if the package is188 consumed by third parties.189- **CI gates** (they block the merge, cheapest first):190 1. `renv::status()` — fails if the lock is out of sync.191 2. `styler` in check mode + `lintr::lint_package()`.192 3. `R CMD check --as-cran` (or `devtools::check()`): **zero ERROR, zero WARNING**; NOTEs are193 justified in writing or fixed.194 4. `testthat` with coverage (`covr`); an agreed threshold — coverage is a signal, not a goal.195 5. Dependency audit (§5) and building the image.196- **CI matrix**: the R version pinned in production, plus the previous one if you support external197 users. Pin the P3M snapshot in CI so that a CRAN release does not break yesterday's build.198199## 5. Stack security200201**`readRDS()` / `load()` / `unserialize()` over untrusted input is code execution.** A serialised R202object can carry environments, promises and classes with methods that run when printed or when203restored. Rule: **never** deserialise an `.rds`/`.RData` that comes from outside your204trust boundary; for exchange use pure data formats (Parquet, CSV, JSON) validated when205read. `load()` also pollutes the global environment — forbidden in package code.206207- **`eval(parse(text = ...))` over user input: FORBIDDEN.** It is R's `eval` and it is the classic208 Shiny vulnerability. Nor `parse()`, `str2lang()`, `source()` of paths built209 with input, nor `do.call(name_as_text, ...)` without an allowlist.210- **Shiny exposes R to the Internet.** Every `input$*` is hostile input:211 - Validate **on the server**, not in the UI: the constraint of a `selectInput` does not exist in212 the protocol, a client can send any value. `validate()`/`req()` are not security213 validation.214 - Never use `input$*` to build SQL, file paths, object names or commands215 (`system()`, `system2()`). An allowlist of permitted values, not a *blacklist*.216 - `fileInput`: a size limit (`shiny.maxRequestSize`), type verified by content, and the217 file processed in a temporary location — never served back and never deserialised.218 - HTML: `HTML()`, `tags$script`, `htmltools::HTML` and `renderText` with `escape = FALSE` are XSS219 if input gets in. By default, escaped text.220 - Authentication: **open source Shiny Server ships no authentication** — it is solved in front221 (reverse proxy with OIDC) or with a product that includes it. Do not implement login in the222 `server()` itself.223- **SQL**: `DBI::dbGetQuery` with `params = list(...)` or `glue::glue_sql()`; `paste0()` of input224 into a query is an absolute veto. With `dbplyr`, review the generated SQL (`show_query()`) — its225 criteria belong to `sql-standards`.226- **`install.packages()` at run time: FORBIDDEN in production.** Installing from the227 started container or from an app's `server()` means the artifact is not immutable,228 that the build depends on the network and that the version running today is not the one that was229 tested. All dependencies are installed at build time, from a pinned snapshot. The same for230 `remotes::install_github()` outside a `Dockerfile` with the commit pinned by SHA.231- **CRAN does not audit security.** CRAN checks that the package *works*, not that it is secure nor232 that its maintainer is still alive. Before adding a dependency: recent maintenance, number of233 maintainers, licence, and whether it drags in a `SystemRequirements` that widens the container's234 surface. A package can execute arbitrary code on installation (`configure`, `.onLoad`).235- **Dependency audit**: the R ecosystem **has no mature equivalent to `pip-audit`**.236 What there is: **`oysteR`** (CRAN 0.1.4, 2025-10-09, Apache-2.0), which queries Sonatype OSS237 Index; the project itself states that it **is not supported by Sonatype** (a community238 contribution) and that heavy use runs into *rate limiting*. Use it as a signal239 (`audit_renv_lock()` in CI, non-blocking at first), complemented with OSV/GitHub Advisories over240 the `renv.lock`, and **assume incomplete coverage**: the absence of findings in R is not evidence241 of the absence of vulnerabilities. Check in §8 whether something better has appeared.242- **Secrets**: never in `.Rprofile`, a versioned `.Renviron`, `renv.lock`, code or reports. Env243 vars or a manager; local `.Renviron` in `.gitignore`. Careful with `.RData` files saved on exit:244 disable automatic session saving (`--no-save`, `--no-restore` in any non-interactive245 run) — an `.RData` with credentials in the repo is a classic incident.246- **Reports**: a rendered Quarto/Rmd embeds whatever you print. Check that no connection247 strings, tokens or personal data come out in the outputs or in the warning messages.248- **Containers**: an image based on `rocker/r-ver` with the R version and snapshot pinned, non-root,249 multi-stage. **The real problem with R in containers is the system dependencies**: many250 packages compile against OS libraries (`libcurl`, `libxml2`, `libssl`, `libgdal`, `libproj`,251 `libgit2`). Install them explicitly in the `Dockerfile` (P3M exposes the `SystemRequirements`);252 do not trust that "they were in the base image". And do not leave them in the final image if they253 were only needed to compile.254255## 6. Performance and operability256257- **Order of attack**, in this order and no other: (1) measure (`profvis`, `bench::mark`) — never258 optimise by intuition; (2) vectorise and eliminate object growth in loops (`x <- c(x, i)` is259 quadratic: preallocate or use `vapply`); (3) `data.table` for heavy aggregation/joins; (4) push260 the computation to `arrow`/`duckdb` or to the database when the data does not fit in RAM; (5)261 `cpp11`/`Rcpp` only for the loop that genuinely cannot be vectorised, and only after 1-4.262- **R copies on modify** and the memory peak is the problem, not the CPU. `data.table` modifies by263 reference (`:=`) — powerful and a source of bugs if the object is shared: document when a264 function mutates its argument, or return an explicit copy.265- Parallelism: `future`/`furrr` or `parallel`. `multicore` (fork) **is not safe on a Shiny/266 Plumber server nor on Windows**: use `multisession` or external processes. Never launch more267 *workers* than the cores assigned to the container — R does not see the cgroup limit on its own.268- **Plumber**: it is **single-threaded**. One slow request blocks everybody. Scale with multiple269 processes behind a load balancer, explicit timeouts on every outbound call270 (`httr2::req_timeout`), and heavy work outside the request. `/healthz` and `/readyz` endpoints;271 structured logging with a *correlation id*.272- **Shiny in production**: each session is state on the server and one R process serves N sessions in273 **a single thread**. Consequences: any long computation in `server()` freezes all the users274 of that process (move it to `future`/a job queue or precompute); session state does not survive275 the process crashing nor migrate between replicas (**session affinity mandatory** on the load276 balancer, and an app that "restarts on its own" is a user losing their work); badly isolated277 reactivity = data leaks between sessions if you put state in the global environment. Size by278 **concurrent sessions and RAM per session**, not by requests/second. Large shared read-only279 objects: load them once outside `server()` (they are shared across sessions of the same process),280 never per-user data.281- DB connections: a *pool* (`pool`) with limits; one connection per Shiny session exhausts itself.282 Always close with `on.exit`.283- Scheduled reports: idempotent, with explicit parameters and versioned output. A report that284 fails must **fail loudly**, not publish yesterday's version.285- Seed and versions in the artifact: every report/model publishes the R version, the `renv.lock` (or286 its hash) and the seed. Without that, a number is not reproducible even if the code is in git.287288## 7. Long-term sustainability289290- **Cadence**: an R minor once a year (spring) — plan it as an event, with reinstallation of291 compiled packages and a full run of the suite. R patches, apply them. P3M snapshot:292 **advance it deliberately** (quarterly, with the suite green) instead of floating or freezing it293 for years; a 3-year-old snapshot is as dangerous as having none, because the day it has to be294 moved the jump is impossible.295- Bioconductor drags the R version along: if you depend on it, your calendar **is theirs** (two296 releases a year), not the other way round.297- Deprecations: tidyverse warns with long cycles but it warns; `lifecycle` badges and298 `DeprecationWarning` are treated as debt with an issue, they are not silenced. `data.table`299 hardly ever breaks its API — that is its value.300- A dependency with no release in >2 years or with a single maintainer is reviewed; if it is on the301 critical path of production, the function you use is vendored or replaced.302- **Debt from an analysis that becomes a service**: when a script starts serving traffic,303 **it is rewritten as a package with tests** before exposing it, not afterwards. "We'll wrap it in304 Plumber and that's it" is the most expensive debt in this ecosystem: nobody knows what inputs it305 accepts, there are no tests, the state lives in the global environment and the first incident is306 at 3 in the morning. If there is no budget for the rewrite, there is no budget for the service:307 publish it as a scheduled report.308309**When NOT to choose R** (honesty first):310- ❌ R as a general-purpose *application* language (transactional backend, system CLI,311 microservice with business logic): use Python, Go or TypeScript.312- ❌ R for pipeline or infrastructure orchestration: that is `data-engineering-standards`.313- ❌ R because "the analyst knows it": if the artifact is a service with an SLA and nobody on the314 team maintains R in production, the correct choice is to port it.315- ✅ **R is the right answer** against Python in: serious statistical modelling (mixed models,316 survival, time series, Bayesian inference, experimental design), biostatistics and317 Bioconductor, publication-quality graphics (`ggplot2`), and reproducible reports where the318 narrative and the computation go together. There R's ecosystem of specialised packages has no319 equivalent.320- ✅ **Python** (see `python-standards`) when the work is *engineering* around the analysis:321 serving, integration, production ML, orchestration, or when the team that will maintain it is an322 engineering one. Pragmatic boundary: if the result is a number or a report, R; if the result is a323 system, Python.324- ✅ **Julia** (see `julia-standards`) only when the bottleneck is a numerical loop that does not325 vectorise. The choice is almost never "R or Julia": R is statistics and communication of results,326 Julia is numerical performance. They share the niche of "a scientific language that is not327 Python" and little else.328329**List of prohibitions (veto):**330- ❌ A project in production without a versioned `renv.lock`, or with a CRAN repository without a331 pinned snapshot.332- ❌ `install.packages()` / `remotes::install_github()` at run time in production.333- ❌ `readRDS()`/`load()`/`unserialize()` over untrusted input. `load()` inside a package.334- ❌ `eval(parse(text = ...))` with user input. `source()` of a path built with input.335- ❌ `setwd()`, `attach()`, `library()` inside `R/`; `rm(list = ls())` as a "restart".336- ❌ Relying on the session's saved `.RData`: always run with `--no-save --no-restore`.337- ❌ SQL via `paste0`. Shiny input into `system()`, paths or object names without an allowlist.338- ❌ `T`/`F` instead of `TRUE`/`FALSE`. `1:n` where `n` may be 0. `sapply()` in production code.339- ❌ `na.rm = TRUE` by reflex, without deciding what `NA` means in that column.340- ❌ Blanket `suppressWarnings()`/`try(silent = TRUE)`; catching and swallowing with no log and no341 re-raise.342- ❌ Retired NSE variants (`aes_string`, `*_` with an underscore) in new code.343- ❌ Business logic inside a `.qmd`/`.Rmd` instead of in `R/` with tests.344- ❌ `R CMD check` with a WARNING and "we'll look at it later"; a NOTE with no written justification.345- ❌ User state in the global environment of a Shiny app (leakage between sessions).346- ❌ Upgrading an R minor while reusing the previous library of compiled packages (see §2).347- ❌ A long synchronous computation inside Shiny's `server()` or a Plumber endpoint.348349## 8. Mandatory web verification350351Before committing to versions or decisions, **verify online** (WebSearch/WebFetch; for versions, the352Atom feeds of GitHub Releases and the CRAN pages — not the summariser over GitHub's HTML):3531. The latest stable R and the last patch of the previous series (`cran.r-project.org/src/base/R-4/`,354 `developer.r-project.org`). As of Aug 2026: **4.6.1 (2026-06-24)** and **4.5.3 (2026-03-11)**.355 **R has no declared LTS** — do not assert it.3562. The Bioconductor version and the R version it is coupled to357 (`bioconductor.org/about/release-announcements/`). As of Aug 2026: **3.23 ↔ R 4.6**.3583. The state of Posit Package Manager (snapshot URL, binary coverage per distro, availability359 of the public service and its terms of use). Public snapshots exist since 2017-10-10 and only360 on working days.3614. Versions and **licences** of `renv`, `styler`, `lintr`, `testthat`, `roxygen2`, `plumber`,362 `data.table` (MPL-2.0, not MIT), `shiny`. Check **CRAN as well as GitHub**: `styler` has363 published no *release* on GitHub since 2024 but its latest CRAN version is 1.11.0 (2025-10-13) —364 a quiet repo ≠ an abandoned package.3655. **Posit's commercial model** (an expensive and changing figure): the `shiny` package is MIT and366 **open source Shiny Server is AGPL-3.0**, but **Shiny Server Pro was discontinued on 2026-03-31**367 and Posit directs you to **Posit Connect**, commercial and licensed by active users;368 anonymous public access to interactive content is a **paid entitlement** (Enhanced/Advanced369 licence). Verify before designing hosting: prices not published, alternatives ShinyProxy (open370 source) and Connect Cloud/shinyapps.io. **Declared discrepancy**: I have found no official371 Posit statement putting open source Shiny Server into maintenance mode, but its last *release*372 on GitHub is from **2024-09-30**; treat its future as an open risk, not as a fact.3736. Vulnerability auditing: the state of `oysteR` and whether a maintained alternative already exists374 (post-check: as of Aug 2026 there is none). **Declared gap, not verified as of Aug 2026**: the real coverage375 of CRAN in OSV/OSS Index (what percentage of packages have advisories) I have not been able to376 quantify — do not assert it.3777. **Declared gap, not verified as of Aug 2026**: the maturity of **S7** as the default object system and378 whether it has already entered base R; and the state of `cpp11` versus `Rcpp` after the header379 change in R 4.6.0.3808. Breaking changes in `roxygen2` 8.x and in R's 4.6 series (graphics API 16→17, headers) from the381 official `NEWS` before any upgrade — never from third-party blogs without cross-checking.382383If the web contradicts this document, **the web wins** — flag the discrepancy.