Contributing to Rceattle
Source:CONTRIBUTING.md
Rceattle fits CEATTLE, a single- and multispecies age-structured stock assessment. Its output sets US federal catch limits. A wrong number here does not crash; it becomes a quota. Every convention below exists so that a change cannot move a fit without someone noticing.
This guide is written for a fisheries scientist or ecologist who wants to fix, extend or understand the package. CLAUDE.md is the coding agent’s operating manual, and holds these rules plus three that bind only an agent — one of them more strictly than this guide does, since an agent must ask before changing an API where you may change one that carries a deprecation path. Where a rule is stated in full there, this guide points to it rather than repeating it.
Setting up
You need R 4.1 or later, a C++ compiler and the packages in DESCRIPTION. The likelihood is a TMB template under src/TMB/; it compiles the first time you load the package.
install.packages(c("devtools", "pkgload", "TMB"))
pkgload::load_all(".") # compiles src/TMB/ceattle.cpp (a few minutes), then loads R/On macOS put the system toolchain first, or a Homebrew clang or gfortran can shadow the one TMB expects:
If clang refuses with “You have not agreed to the Xcode license”, set DEVELOPER_DIR=/Library/Developer/CommandLineTools in the same shell. Compiled artifacts (*.o, *.so, *.dll) are gitignored. Never commit them.
Edits to R code are picked up by pkgload::load_all(".", compile = FALSE). Edits to src/TMB/*.cpp or *.hpp are inert until pkgload::load_all(".") recompiles.
Running the tests
NOT_CRAN=true turns on the tests that fit real models; without it most of the suite is skipped. TESTTHAT_PARALLEL=false is required after a C++ change, because the parallel workers cannot load a freshly rebuilt DLL.
To run one file with the shared helpers loaded:
pkgload::load_all(".", quiet = TRUE)
e <- new.env(parent = asNamespace("Rceattle")) # plain new.env() cannot see internal helpers
for (f in list.files("tests/testthat", "^helper", full.names = TRUE)) sys.source(f, envir = e)
testthat::test_file("tests/testthat/test-selectivity-logisticpm.R", env = e)Three checks sit outside the ordinary suite:
-
The golden regression (
tests/testthat/test-golden-regression.R) evaluates four reference models at stored parameters and pins their objectives and the derived quantities that set catch advice (SSB, biomass, recruitment, SB0, dynamic B0, M2). Any change that can move a fit must leave them unchanged, or re-pin them with the objective changes inNEWS.md. It runs only underNOT_CRAN=true. -
tools/verify/holds harnesses for what the golden fits cannot see: refit paths, simulation draws, MSE reproducibility.tools/README.mdsays which to run for which change. -
inst/dev/TRAPS.mdlists the ways a change has silently gone wrong before, with the measured numbers. Read it before touching anything it names.
Where to read first
-
vignettes/articles/developer-guide.Rmd: the fit pipeline, the switch system, the column schema and the linkage grammar. -
vignettes/articles/adding-a-selectivity-form.Rmd: one extension traced end to end, with the files it touches and the tests that catch a half-finished job. -
R/0-column_schema.R: the source of truth for every workbook column and switch value. - The C++ reference on the package site (built from the Doxygen comments in
src/TMB/): the model equations, function by function.src/TMB/spr.hppis the header to emulate when you document C++ — one of the four that carry a file block.
AGENTS.md at the repository root is the tool-neutral version of this, for an AI assistant that does not read CLAUDE.md.
Making a change
Branches. Cut a branch from dev and open a pull request into dev. The maintainer merges dev into main as a release, following inst/RELEASE-CHECKLIST.md. Operational assessments pin a tagged release, so main moves only at a release.
What a pull request owes. A behaviour, API or documentation change updates NEWS.md, the DESCRIPTION version and the affected vignette together (what counts as breaking, and the _pkgdown.yml and man/ obligations, are rules 5 and 6 in CLAUDE.md). Add a test named tests/testthat/test-<area>-<topic>.R; a test that runs a real optimization starts with testthat::skip_on_cran().
Commit messages. Plain text, imperative subject, a body that says why; rule 13 in CLAUDE.md is the full statement.
Continuous integration. A pull request into dev runs R CMD check on macOS, Windows and three Ubuntu R versions, plus six test-coverage shards. The package site (pkgdown.yaml) builds only for main, so check a documentation change locally with pkgdown::build_reference_index(pkgdown::as_pkgdown(".")) and, for an article, pkgdown::build_article("<name>").
The rules that constrain a change
Numbered as in CLAUDE.md’s “Hard rules”, which states rule 1 more strictly for an agent — it must ask before an API change, where you need only the deprecation path below. In brief:
- Preserve the public API. Deprecate a function or argument; do not delete it unless the maintainer directs it, and record that direction in
NEWS.md. You may change one — rename it, or move a default — but it owes a deprecation path that keeps old fits working, aNEWS.mdentry, and a sweep of the repositories ininst/dev/SIBLING-REPOS.md. Giving a switch a new meaning has no such path, so it is a breaking change: classify it as one. (A coding agent works under a stricter version of this and has to ask first; see doctrine 1 inCLAUDE.md.) - A change that can move a fit needs the golden regression, and the
tools/verify/harness that covers what golden cannot. - The column schema defines every switch value, default and column order. Read them from it; never hardcode one elsewhere.
- Do not invent a switch code, a default or a unit. If it is not in the schema or a vignette, ask.
-
fleet_control$Fleet_codeequals the row number, and selectivity bin columns follow two conventions (ordinal versus absolute age).nagesis a count of bins, not the oldest age. -
src/TMB/linkage.hppandR/0-linkage_encode.Rhold the same process and parameter codes; change one, change both. - The repositories in
inst/dev/SIBLING-REPOS.mdconsume this API. Sweep them after a breaking change.
Code style
Follow the file you are editing. Where it does not settle the question, R follows the tidyverse style guide and C++ the Google C++ style guide: snake_case, two-space indents, about 80 columns, full words over abbreviations. Neither guide is a reason to reformat code you did not otherwise need to touch, and TMB idiom outranks both — the C++ keeps its snake_case function names.
Four habits are specific to this package:
- Read a list or a
fleet_controlcolumn with[[ ]], not$.$partial-matches without a warning, so whereTime_varying_selis missing,fleet_control$Time_varying_selhands backTime_varying_sel_sd. Ten pairs among the schema’s columns have that shape. In new code only: the existing$reads are safe because of which accessor runs first, and rewriting one of them as a bare$would trade a loud failure for a silent one. - Add nothing the change does not need now — no helper, wrapper, class, config layer or option flag for later.
CLAUDE.md’s doctrine 2 states this in full, with what earns a helper its place and why an unreachable guard is not safety. But registering in a registry that already exists is the opposite, and is not optional: a newjnll_comprow owes theJnllRowpartners and.JNLL_ROW_AXIS, and a newIndex_distributionfamily owes.index_rows_natural_scale(), plus.index_fleets_covariance()if it carries a covariance rather than a scalar sd — miss either and the family silently gets the log-scale residual, or a denominator its likelihood never read. - Write in the idiom of the file you are in, and do not convert one to the other. The plotters and
R/5-rearrange_data.Ruse dplyr and the pipe; most ofR/1-*toR/6-*is base R. Inside the fitting pipeline a rewrite is how a fit moves silently — a verb that reorders rows, drops a dimension name or returns a tibble does not error, and the C++ template reads the type it gets. - Keep a pull request to one concern, and keep reformatting out of a change that can move a number. A reviewer checking a fit change should not have to read past whitespace to find it. Ask before adding a dependency.
Writing comments and documentation
Write for a fisheries scientist who was not in the room: the assessment reason, the units and the convention, in one or two lines. The “Comments” and “Domain vocabulary” sections of CLAUDE.md give the rules and a before-and-after example; src/TMB/spr.hpp is the C++ header to emulate.
Getting help
Open an issue at https://github.com/afsc-assessments/Rceattle/issues so the answer is searchable. The maintainer address in DESCRIPTION is the fallback.