diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json new file mode 100644 index 0000000..75f359b --- /dev/null +++ b/.claude-plugin/marketplace.json @@ -0,0 +1,15 @@ +{ + "name": "bioc-package-dev", + "description": "Tooling for developing, maintaining, submitting, and reviewing Bioconductor R packages.", + "owner": { + "name": "ybaeus", + "url": "https://github.com/ybaeus/bioc_package_dev" + }, + "plugins": [ + { + "name": "bioconductor-package-dev", + "source": "./", + "description": "Guidance and review for developing, maintaining, submitting, and reviewing Bioconductor R packages." + } + ] +} diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..c27068f --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,11 @@ +{ + "name": "bioconductor-package-dev", + "description": "Guidance and review for developing, maintaining, submitting, and reviewing Bioconductor R packages, distilled from the official Bioconductor contributions guide.", + "version": "0.1.0", + "author": { + "name": "ybaeus" + }, + "homepage": "https://github.com/ybaeus/bioc_package_dev", + "repository": "https://github.com/ybaeus/bioc_package_dev", + "license": "Apache-2.0" +} diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml new file mode 100644 index 0000000..62115ac --- /dev/null +++ b/.github/workflows/verify.yml @@ -0,0 +1,223 @@ +name: verify + +# Four jobs, deliberately on four different triggers. The static checks are cheap enough to gate +# every push. The golden path costs a Bioconductor install, so it runs on pull requests and on a +# schedule. The fidelity job never gates a pull request: upstream changing is a reason to open an +# issue, not a reason to block somebody's work. Evals cost tokens and run only when asked. + +on: + push: + branches: [main, dev] + pull_request: + schedule: + # Mondays, 06:00 UTC. + - cron: "0 6 * * 1" + workflow_dispatch: + inputs: + run-evals: + description: "Also run the behavioral evals (costs tokens)" + type: boolean + default: false + +permissions: + contents: read + +jobs: + static: + name: static checks + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + # verify.py compares each knowledge file's Fetched stamp against the date of the commit + # that last touched it, which a shallow clone cannot answer. + fetch-depth: 0 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - run: python3 scripts/verify.py + + fidelity: + name: upstream fidelity + if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + permissions: + contents: read + issues: write + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Check against upstream + id: check + continue-on-error: true + # `shell: bash` brings -o pipefail, without which tee would mask the failing exit status. + shell: bash + run: python3 scripts/verify.py --network --json | tee fidelity.json + + - name: Open or update the drift issue + if: steps.check.outcome == 'failure' + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + title="Upstream drift detected by the fidelity job" + body=$(mktemp) + { + echo "\`scripts/verify.py --network\` failed on $(date -u +%Y-%m-%d)." + echo + echo "Run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" + echo + echo "Work these top-down - the ordering in \`docs/REFRESH.md\` is by blast radius:" + echo + echo '```' + python3 -c "import json;d=json.load(open('fidelity.json'));[print('-',f) for f in d['failures']];[print('- warn:',w) for w in d['warnings']]" + echo '```' + } > "$body" + + existing=$(gh issue list --state open --label upstream-drift --json number --jq '.[0].number') + if [ -n "$existing" ]; then + gh issue comment "$existing" --body-file "$body" + else + gh label create upstream-drift --color FBCA04 --description "Tracked upstream moved" \ + --force >/dev/null 2>&1 || true + gh issue create --title "$title" --label upstream-drift --body-file "$body" + fi + + - name: Fail the job if upstream drifted + if: steps.check.outcome == 'failure' + run: exit 1 + + golden-path: + name: golden path + # Not on push: a Bioconductor devel install plus build, check and BiocCheck costs several + # minutes. Pull requests and the weekly cron are enough. + if: github.event_name != 'push' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: grimbough/bioc-actions/setup-bioc@v1.0.16 + with: + bioc-version: devel + + # setup-bioc does not install pandoc, and the biocthis vignette template is R Markdown, so + # R CMD build fails at "creating vignettes" without this. + - uses: r-lib/actions/setup-pandoc@v2 + + # Deliberately NOT using grimbough/bioc-actions/use-bioc-caches. It pins actions/cache@v2, + # which GitHub now auto-fails, so the whole job dies in "Set up job" before running a step. + # Still broken on the action's main branch as of 2026-08-14, so a tag bump will not fix it. + # No loss here: that action caches BiocFileCache/ExperimentHub/AnnotationHub/biomaRt data, + # and the golden-path package downloads none of it. What is worth caching is the R library. + - name: Resolve the R library path + id: rlib + run: echo "path=$(Rscript -e 'cat(.libPaths()[1])')" >> "$GITHUB_OUTPUT" + + - uses: actions/cache@v4 + with: + path: ${{ steps.rlib.outputs.path }} + key: bioc-devel-${{ runner.os }}-${{ hashFiles('.github/workflows/verify.yml') }} + restore-keys: bioc-devel-${{ runner.os }}- + + - name: Install the tools this repo recommends + run: | + Rscript -e 'BiocManager::install(c( + "biocthis", "BiocCheck", "roxygen2", + "BiocStyle", "knitr", "RefManageR", "sessioninfo", "testthat", + "SummarizedExperiment" + ), ask = FALSE)' + + # Built outside the checkout: usethis::create_package() refuses to create a project nested + # inside an existing one, and the workspace is this repository. + - name: Run the documented scaffolding chain + shell: bash + run: | + if ! Rscript scripts/golden-path.R "${{ runner.temp }}/GoldenPathPkg" 2>&1 \ + | tee golden-path.log; then + echo "::error::golden-path failed: $(tail -c 900 golden-path.log | tr '\n' ' ')" + exit 1 + fi + + # Built here rather than only inside the action so the diagnostic reaches the job + # annotation instead of being buried in a log that needs a token to read. + - name: Build the scaffolded package + shell: bash + working-directory: ${{ runner.temp }} + run: | + if ! R CMD build GoldenPathPkg > build.log 2>&1; then + echo "::error::R CMD build failed: $(tail -c 1800 build.log | tr '\n' ' ')" + exit 1 + fi + + - uses: grimbough/bioc-actions/build-install-check@v1.0.16 + with: + package-directory: ${{ runner.temp }}/GoldenPathPkg + + - uses: grimbough/bioc-actions/run-BiocCheck@v1.0.16 + with: + package-directory: ${{ runner.temp }}/GoldenPathPkg + error-on: error + # --no-check-bioc-help skips the support-site registration and bioc-devel subscription + # checks. Those are ERRORs and they can never pass for a throwaway fixture: they test + # whether the maintainer email is registered with Bioconductor, and this package's + # maintainer is a usethis placeholder. Nothing else is disabled - the point is to find + # out whether the documented chain produces an acceptable package, so weakening the + # checks any further would defeat the exercise. + arguments: --no-check-bioc-help + + # BiocCheck exits with a bare status code, so without this a red build says only "exit + # code 2" and reading the reason needs a token. + - name: Surface the BiocCheck log on failure + if: failure() + shell: bash + run: | + found=$(find "$RUNNER_TEMP" "$GITHUB_WORKSPACE" -name '*.BiocCheck' -o -name '00BiocCheck.log' 2>/dev/null | tr '\n' ' ') + echo "::error::BiocCheck artifacts: ${found:-none}" + log=$(find "$RUNNER_TEMP" "$GITHUB_WORKSPACE" -name '00BiocCheck.log' 2>/dev/null | head -1) + if [ -n "$log" ]; then + echo "::error::BiocCheck head: $(head -c 2200 "$log" | tr '\n' ' ')" + echo "::error::BiocCheck error lines: $(grep -in -A3 'error' "$log" | head -c 1800 | tr '\n' ' ')" + fi + + evals: + name: behavioral evals + if: github.event_name == 'workflow_dispatch' && inputs.run-evals + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Require an API key + env: + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + run: | + if [ -z "${ANTHROPIC_API_KEY:-}" ]; then + echo "ANTHROPIC_API_KEY is not set - add it as a repository secret." >&2 + exit 1 + fi + + - uses: actions/setup-node@v4 + with: + node-version: "22" + + - run: npm install -g @anthropic-ai/claude-code + + - name: Validate the plugin manifests + run: claude plugin validate . + + - name: Run the eval suite + env: + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + # --scaffold runs each case's scaffold_script, which builds the broken fixture package + # the two agent cases audit. The cases in this repository were authored here. + run: claude plugin eval . --ablation with-without --scaffold --report eval-report.html + + - uses: actions/upload-artifact@v4 + if: always() + with: + name: eval-report + path: eval-report.html diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0774abe --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +# Working docs - local only, not shipped +context/ + +# OS +.DS_Store +Thumbs.db +evals/results/ + +# Python +__pycache__/ +*.pyc diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..99c0047 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,124 @@ +# Bioconductor package development - agent instructions + +Cross-tool entrypoint (read natively by Codex, Cursor, Gemini CLI, Copilot, and others). When +the task involves developing, maintaining, submitting, or reviewing a Bioconductor package, +follow the rules here and open the matching file under `knowledge/` for detail. The `knowledge/` +directory is the single source of truth; this file is a short router over it. + +## When this applies +Any work on: an R package intended for Bioconductor; `DESCRIPTION` / `NAMESPACE` / `NEWS` / +`biocViews` / `BiocCheck`; S4 or Bioconductor core classes (e.g. SummarizedExperiment); +vignettes and man pages for a Bioc package; submission to the Bioconductor Contributions +tracker; the Bioconductor git server (git.bioconductor.org). It applies even when the user does +not say "Bioconductor" explicitly but the package clearly targets it. + +## Router +- Full submission path, start to finish: `knowledge/workflow.md` +- Topic map across all chapters: `knowledge/index.md` +- Submission mechanics + package types: `knowledge/01-submissions.md` +- Authoring topics (naming, metadata, docs, data, tests, R code, compiled code, shiny, etc.): + `knowledge/development/` +- Maintenance (git server, versioning, build reports, deprecation): `knowledge/maintenance.md` +- What reviewers check: `knowledge/reviewer.md` +- Appendices (devel Bioc, build options, C/Fortran, etc.): `knowledge/appendices.md` + +## Pre-submission gate +Two tiers, because upstream states them at two different strengths. Do not report a tier-2 item +as a blocker; report it as something a reviewer will very likely ask about. + +Tier 1 - stated as requirements: +- `R CMD check` and `BiocCheck` pass with no ERROR and no WARNING on current R-devel. This is the + tracker's own wording: "a minimum requirement for package acceptance". It also says "Passing + these checks does not result in automatic acceptance" - a human review follows. +- Run both entry points: `BiocCheck::BiocCheckGitClone()` and + `BiocCheck::BiocCheck('new-package' = TRUE)`. +- Individual files must be <= 5 MB. Upstream states this one as "must". +- `biocViews` present; a vignette and man pages present; maintainer email valid and belonging to + the person submitting; not on CRAN ("a package can only be submitted to one or the other"); + hosted on the GitHub default branch. BiocCheck catches most of these. + +Tier 2 - stated as should or recommended: +- `Version: 0.99.0` for a new package (upstream: "should set"). Expected in practice; set it. +- Source build under 10 MB (upstream: "should occupy less than"). +- `R CMD check --no-build-vignettes` under 10 min (upstream: "should require less than"). +- Vignettes, examples and tests under 8 GB memory (upstream: "it is recommended that"). + +Detail: `knowledge/development/build-check-bioccheck.md` and `knowledge/development/general-dev.md`. + +## Version rule +Start `0.99.0`. Scheme `x.y.z`: `y` odd in devel, even in release (max 99); bump `z` by 1 on +every commit; `0.99.z` becomes `1.0.0` at the first Bioconductor release; `x` changed only by +the Bioconductor team. Detail: `knowledge/maintenance.md`. + +## Bioconductor code style (differs from tidyverse) +Use `<-` for assignment, 4-space indentation, 80-column lines; prefer vectorized code; avoid +`1:n` (use `seq_len`/`seq_along`). Detail: `knowledge/development/r-code.md`. + +## Submitting and the git server (short) +Host on the GitHub default branch, then open an issue (title = package name) at +https://github.com/Bioconductor/Contributions/issues/new (Annotation packages: email +packages@bioconductor.org). The Single Package Builder must pass on all platforms. After +acceptance, register an SSH key at BiocCredentials, add `upstream = git.bioconductor.org`, and +push to both remotes; only `devel` and `RELEASE_x_y` branches accept pushes. Full sequence: +`knowledge/workflow.md`. + +## Tooling (use these, do not reimplement them) +This repo ships no validator and no templates on purpose - Bioconductor already maintains both, +and reusing existing infrastructure is itself a review criterion (ch 5). + +```r +# Validation - BiocCheck is authoritative +BiocCheck::BiocCheckGitClone() +BiocCheck::BiocCheck('new-package' = TRUE) + +# Scaffolding - biocthis writes Bioconductor-shaped files +biocthis::use_bioc_description(biocViews = "Software, ") +biocthis::use_bioc_news_md() +biocthis::use_bioc_vignette(name = "", title = "Introduction to ") +biocthis::use_bioc_citation() +biocthis::use_bioc_github_action() +``` + +`use_bioc_description()` writes a **fresh** DESCRIPTION; it does not merge into an existing one. +Internally it calls `usethis::use_description()`, which calls `write_over()`, which asks before +replacing an existing file - and in a non-interactive session it declines silently. So for a +package that already has a DESCRIPTION, this call very often does nothing at all and you get no +error. Add `biocViews` by hand instead, or approve the overwrite knowing it discards the +DESCRIPTION you have. Everything else in the chain appends and is safe on an existing package. + +`biocViews = "Software"` on its own is a BiocCheck **ERROR**: "Add biocViews other than Software". +The top-level terms (Software, AnnotationData, ExperimentData, Workflow) do not count on their +own - pick specific terms from the vocabulary at +https://bioconductor.org/packages/release/BiocViews.html, e.g. +`"Software, GeneExpression, Transcriptomics"`. Two other things BiocCheck flags on a freshly +scaffolded package: the placeholder Description is "too concise" (it wants at least three +sentences), and a Software package with no Bioconductor dependencies gets a warning suggesting +CRAN instead. + +`use_bioc_citation()` leaves `inst/CITATION` unfinished, and unfinished here means broken. The +template substitutes `{{Title}}` and `{{github_owner}}`; the function passes neither a `Title` +nor - on any package that has no GitHub remote configured yet - an owner. The file lands with an +empty title and an empty author, `utils::citation()` errors on either ("a bibentry of bibtype +'Manual' has to specify the field: title"), and because the generated vignette calls `citation()`, +`R CMD build` fails at "creating vignettes". Verified against biocthis 1.23.0 on 2026-08-14. Fill +in the title, the author, and the placeholder `10.1101/TODO` DOI before building anything. + +Install with: + +```r +BiocManager::install(c( + "BiocCheck", "biocthis", + # use_bioc_vignette() adds these to Suggests and refuses to run unless they are installed + "BiocStyle", "knitr", "RefManageR", "sessioninfo", "testthat" +)) +``` + +BiocCheck cannot measure the two +timing gate items (`R CMD check --no-build-vignettes` under 10 min, under 8 GB memory) - those +need a real build. + +Current cycle: Bioconductor release 3.23, devel 3.24, both on R 4.6.0. Build against devel for a +new submission. Never guess this pair - it changes twice a year, `knowledge/SOURCES.md` records +what was verified and when, and https://bioconductor.org/config.yaml is authoritative. + +Canonical guide: https://contributions.bioconductor.org (source: github.com/Bioconductor/pkgrevdocs). diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..a71087b --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,8 @@ +# Project instructions + +Bioconductor package-development guidance lives in the cross-tool instructions file. Follow it: + +@AGENTS.md + +For Claude Code specifically, the same guidance is packaged as a skill +(`bioconductor-package-dev`) and a review agent (`bioc-package-review`) - see README.md. diff --git a/GEMINI.md b/GEMINI.md new file mode 100644 index 0000000..f0518b6 --- /dev/null +++ b/GEMINI.md @@ -0,0 +1,5 @@ +# Project context + +Bioconductor package-development guidance lives in the cross-tool instructions file. Follow it: + +@AGENTS.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..03bcc5a --- /dev/null +++ b/README.md @@ -0,0 +1,194 @@ +# bioc_package_dev + +Reusable AI-assistant tooling for developing, maintaining, submitting, and reviewing +Bioconductor R packages to the project's official standards. The knowledge is distilled from the official guide "Bioconductor Packages: Development, Maintenance, and Peer Review" (https://contributions.bioconductor.org) into portable, task-oriented summaries that any AI +coding assistant - or a human - can follow. + +It ships in two forms from one source of truth: +- A Claude Code plugin: a skill (`bioconductor-package-dev`) plus a review agent + (`bioc-package-review`). +- A cross-tool `AGENTS.md` that Codex, Cursor, Gemini CLI, GitHub Copilot, and other assistants + read natively. + +## Repository layout + +``` +bioc_package_dev/ +├── README.md # this file +├── AGENTS.md # cross-tool entrypoint (Codex/Cursor/Gemini/Copilot) +├── GEMINI.md # imports AGENTS.md (for Gemini CLI) +├── CLAUDE.md # imports AGENTS.md (for Claude Code) +├── LICENSE # Apache-2.0 +├── knowledge/ # single source of truth - portable markdown summaries +│ ├── index.md # topic router across all summaries +│ ├── workflow.md # end-to-end runbook, incl. converting an existing package +│ ├── SOURCES.md # .Rmd -> slug -> file map + pins for all tracked upstreams +│ ├── 01-submissions.md # ch 1 +│ ├── development/ # ch 2-23 (naming, metadata, docs, data, tests, code, ...) +│ ├── maintenance.md # ch 24-30 +│ ├── reviewer.md # ch 31-33 +│ └── appendices.md # A-H +├── docs/REFRESH.md # how to re-sync with upstream when it moves +├── scripts/ # verification, not package tooling (see "How this is verified") +├── evals/ # behavioral test cases for the skill and agent +├── .github/workflows/ # CI running the verification layers +├── .claude-plugin/ # Claude Code plugin + marketplace manifests +├── skills/bioconductor-package-dev/SKILL.md +└── agents/bioc-package-review.md +``` + +There is deliberately no templates directory and no check script. Bioconductor already +maintains both - `biocthis` for scaffolding, `BiocCheck` for validation - and reusing existing +infrastructure instead of reinventing it is one of the things reviewers look for (ch 5). Shipping +a competing copy would have made this repo violate the guidance it teaches. + +## Prerequisites + +For the guidance itself, none - it is markdown. To actually build and check a package you need: +- R (current release, plus R-devel for the final submission check). +- `BiocManager::install(c("BiocCheck", "biocthis", "BiocStyle", "knitr", "RefManageR", + "sessioninfo", "testthat", "roxygen2"))` - BiocCheck validates and biocthis scaffolds; the rest + are what `biocthis::use_bioc_vignette()` refuses to run without. The exact list, and the traps + in the scaffolding chain, are in the tooling block in `AGENTS.md`. +- Pandoc, if you want to build an R Markdown vignette locally. +- Optional: `usethis`, `devtools`. + +To run this repo's own verification you need Python 3 (stdlib only) for `scripts/verify.py`; the +network layer additionally needs outbound HTTPS. No R is required for the static layer. + +## Install and use + +### Claude Code (plugin) + +One-command install from this repo's marketplace: + +``` +/plugin marketplace add ybaeus/bioc_package_dev +/plugin install bioconductor-package-dev +``` + +Then, in a package project, the skill triggers automatically on Bioconductor work, or invoke it +with `/bioconductor-package-dev`. For a submission-readiness audit, ask Claude to "review my +package for Bioconductor submission" (runs the `bioc-package-review` agent). + +To try it before installing, clone this repo and run: + +``` +claude --plugin-dir /path/to/bioc_package_dev +``` + +### Codex / Cursor / Gemini CLI / GitHub Copilot (AGENTS.md) + +These tools read `AGENTS.md` natively. Either work inside a clone of this repo, or copy +`AGENTS.md` and the `knowledge/` directory into your package project. Gemini CLI also reads +`GEMINI.md` (which imports `AGENTS.md`). + +### Any tool, or a human (manual) + +Read `knowledge/index.md` to find the topic, or `knowledge/workflow.md` for the full submission +path. Point any assistant at the `knowledge/` directory. + +## Example prompts + +Written for the main case: you already have R work on GitHub - a package, or just analysis code - +and want to contribute it to Bioconductor. Ask in your own words - these are shapes, not +incantations. + +Getting oriented: + +- "I have an R package on GitHub, what do I need to do to submit it to Bioconductor?" +- "I have this crufty analysis code, review it and make it into a Bioconductor-submittable package" +- "My package is on CRAN, can I move it to Bioconductor?" +- "What version number do I use for a new submission?" + +Working through specifics: + +- "I have 300 MB of reference data, where does it go?" (answer: not in the package) +- "Write me a function that iterates over samples" (applies Bioconductor style, not tidyverse) +- "My DESCRIPTION has no biocViews - what do I put there?" +- "Walk me through what happens after I open the Contributions issue." + +Checking readiness - these route to the `bioc-package-review` agent, which audits and reports +blockers rather than advising as you work: + +- "Audit my package for Bioconductor submission readiness." +- "Would this package pass review? Tell me what a reviewer would flag." + +The split in one line each: the skill guides you while you work; the agent renders a verdict on +demand. + +## Keeping it current + +The summaries are stamped with the date they were generated from the live guide, and +`knowledge/SOURCES.md` pins the exact upstream `pkgrevdocs` commit they came from. Bioconductor +updates the guide roughly twice a year with each release. Follow `docs/REFRESH.md`: diff the +current upstream commit against the pinned one, regenerate only the changed chapters, bump +`version` in `.claude-plugin/plugin.json`, and push. Marketplace users then run +`/plugin marketplace update` and `/plugin update bioconductor-package-dev`. The summaries always +link back to the canonical chapter, which is the authority if anything drifts. + +Five upstreams are tracked, not just the guide: `pkgrevdocs`, the Contributions issue template, +BiocCheck, biocthis, and bioc-actions. All five pins live in `knowledge/SOURCES.md`, and the +weekly `fidelity` CI job opens an issue when any of them moves. + +## How this is verified + +Prose summarizing a live document rots quietly: a URL dies, upstream rewords a rule, a threshold +drifts between the files that restate it, or a reworded skill `description` stops the skill from +firing while every file still looks fine. Four layers catch different failures. + +| Layer | Command | Catches | +|---|---|---| +| Static | `python3 scripts/verify.py` | broken internal paths, gitignored references, manifest and frontmatter errors, missing `Source:`/`Fetched` stamps, rule text that has drifted between the files that duplicate it, emoji, a stale README tree | +| Fidelity | `python3 scripts/verify.py --network` | upstream commit drift mapped to the affected summaries, dead chapter URLs, thresholds that no longer match upstream, summaries that harden an upstream "should" into a "must", gate wording that no longer matches the submission tracker, new upstream chapters nobody summarized | +| Golden path | `Rscript scripts/golden-path.R` + `R CMD build` + `BiocCheck` in CI | the scaffolding commands this repo tells you to run, by running them and checking the result with real Bioconductor tooling | +| Behavior | `claude plugin eval . --scaffold` | the skill firing when it should, staying quiet when it should not, and returning the right values - including every prompt in "Example prompts" above | + +The fidelity job is a weekly cron rather than a PR gate: upstream changing is a reason to open an +issue, not to block someone's pull request. + +The golden path earns its keep. On its first real run it found five ways the documented +instructions failed: `use_bioc_vignette()` needs BiocStyle and friends actually installed, not +just declared; `use_bioc_description()` silently declines to touch an existing DESCRIPTION; +`use_bioc_citation()` writes an `inst/CITATION` with an empty title and author that makes +`R CMD build` fail; and `biocViews = "Software"` on its own is a BiocCheck error. Every one of +those would have been hit by a user following this repo's advice, and none of them is visible by +reading the files. It now passes end to end against Bioconductor devel. + +## Built on + +This repo is a thin layer over other people's work. It contributes routing, summarization, and +verification; everything substantive below belongs to the projects listed here, and the design +rule throughout has been to point at existing Bioconductor infrastructure rather than ship a +competing copy of it. + +- **Bioconductor Packages: Development, Maintenance, and Peer Review** - Kevin Rue-Albrecht, + Daniela Cassol, Johannes Rainer, Lori Shepherd, Marcel Ramos Pérez, Martin Morgan. + https://contributions.bioconductor.org, source + [Bioconductor/pkgrevdocs](https://github.com/Bioconductor/pkgrevdocs). Every file under + `knowledge/` is derived from it, cites the chapter it came from, and defers to it on any + disagreement. +- **[BiocCheck](https://github.com/Bioconductor/BiocCheck)** - Lori Shepherd, Marcel Ramos, and + the Bioconductor core team. The authoritative validator. This repo runs it and reads its output + instead of reimplementing its checks. +- **[biocthis](https://github.com/lcolladotor/biocthis)** - Leonardo Collado-Torres. Scaffolding + that writes Bioconductor-shaped package files. This repo recommends it rather than shipping + templates, and CI runs the exact command sequence it recommends. +- **[bioc-actions](https://github.com/grimbough/bioc-actions)** - Mike Smith. Composite GitHub + Actions for setting up Bioconductor, building, checking, and running BiocCheck. They power the + golden-path job, which is what lets this repo claim its instructions actually work. +- **[Bioconductor/Contributions](https://github.com/Bioconductor/Contributions)** - the submission + tracker, and the source of the authoritative pre-submission checklist that the gate here is + checked against. + +Thanks to all of them. Errors in the summaries are this repo's, not theirs - report them here, and +consult the linked chapter as the authority. + +## Attribution and license + +The summaries are derived from the Bioconductor contribution guide, source repository +[Bioconductor/pkgrevdocs](https://github.com/Bioconductor/pkgrevdocs) and the rendered guide at +https://contributions.bioconductor.org. That upstream material belongs to the Bioconductor +project; each summary links to its canonical chapter. This repository's own tooling is released +under Apache-2.0 (see `LICENSE`). "Bioconductor" is a trademark of the Bioconductor project; +this project is not affiliated with or endorsed by Bioconductor. diff --git a/agents/bioc-package-review.md b/agents/bioc-package-review.md new file mode 100644 index 0000000..7d399bc --- /dev/null +++ b/agents/bioc-package-review.md @@ -0,0 +1,156 @@ +--- +name: bioc-package-review +description: >- + Audits a package against Bioconductor's submission requirements and reports what would block + acceptance, before a reviewer sees it. Use when the user asks to review, audit, or check whether + their package is ready to submit, asks what a reviewer would flag, or wants a pre-submission + check. Walks the package against the official pre-submission gate and the contribution + guidelines, then returns a structured blockers/warnings/suggestions report mapped to specific + guide chapters and ending in a verdict. Read-only: it renders a verdict, it does not fix. +model: fable +tools: Read, Grep, Glob, Bash +--- + +# Bioconductor package review agent + +You audit an R package against the Bioconductor contribution standards and report whether it is +submission-ready. You are read-mostly: inspect files and run read-only checks; do not modify the +package. Your knowledge base is the plugin's `knowledge/` directory +(`${CLAUDE_PLUGIN_ROOT}/knowledge/`) - consult `reviewer.md`, `workflow.md`, and the development +chapters and cite chapter numbers in findings. + +## What to inspect +Locate the package root (the directory containing `DESCRIPTION`). + +If there is no `DESCRIPTION` anywhere, the target is not a package yet - loose scripts, an +analysis repo, a bag of `.R` files. Do not report that as a blocker list against the gate; almost +every item would fail and the report would be noise. Say plainly that this is not yet a package, +then give a short gap report instead: whether the code looks like a Software package or a Workflow +package (`knowledge/development/non-software-pkgs.md`), what would have to become exported +functions, which top-level side effects have to go (`setwd()`, `rm(list = ls())`, +`install.packages()`, hardcoded paths), and where the data would live. Point at +`knowledge/workflow.md`, "Converting existing work", for the sequence. Verdict line becomes +"Not a package yet - N things to do first". + +With a `DESCRIPTION` present, check: + +Metadata (`knowledge/development/metadata-files.md`, ch 6-12): +- `DESCRIPTION`: `Version: 0.99.0` for a new package; `biocViews` present and valid; + `Authors@R` with a maintainer (`cre`) and valid email; `Title`, `Description`, `License` + present; sane `Imports`/`Depends`/`Suggests` (avoid Depends bloat). +- `NAMESPACE`: explicit exports and `importFrom`; no `import()` of whole large packages without + reason. +- `NEWS`/`NEWS.md`, `README`, `LICENSE`, `inst/CITATION` present and well-formed. + +Documentation (`knowledge/development/documentation.md`, ch 13): +- A vignette under `vignettes/` (evaluated, not a stub); man pages for exported objects, with + runnable examples (flag `dontrun`/`donttest` overuse). + +Code and tests (ch 15-16): +- Unit tests present under `tests/`. +- Bioconductor code style: `<-`, 4-space indent, 80-col; flag `1:n` (prefer `seq_len`/`seq_along`), + `sapply` where `vapply` is safer, `T`/`F`, `<<-`, and `.Internal`/`.Call` misuse. + +Data and size (ch 14, 21): +- No large data files; individual files <= 5 MB; large data belongs in ExperimentHub/AnnotationHub. +- `.gitignore` present; no build artifacts, tarballs, or hidden junk committed. + +Reuse (ch 5): uses standard Bioc classes (SummarizedExperiment, GRanges, etc.) where appropriate +rather than reinventing them. + +## The gate (report each as pass/fail, in two tiers) +Upstream states these at two different strengths, and your report must preserve that. A tier-2 +miss is a warning, never a blocker - calling it a blocker tells the user they cannot submit when +Bioconductor would accept them. + +Tier 1 - requirements: +- `R CMD check` and `BiocCheck` pass with no ERROR and no WARNING on R-devel. The tracker calls + this "a minimum requirement for package acceptance", and adds that passing "does not result in + automatic acceptance". +- `BiocCheck::BiocCheckGitClone()` clean. +- `BiocCheck::BiocCheck('new-package' = TRUE)` clean. +- Individual files <= 5 MB (upstream states this as "must"). +- `biocViews`, vignette and man pages present; maintainer email valid and equal to the submitter; + not on CRAN; hosted on the GitHub default branch. + +Tier 2 - should or recommended: +- `Version: 0.99.0` for a new package. Expected in practice; flag a wrong version prominently, + but as a warning. +- Source build under 10 MB; `R CMD check --no-build-vignettes` under 10 min; under 8 GB memory. + +## Tooling (use these, do not reimplement them) +This repo ships no validator and no templates on purpose - Bioconductor already maintains both, +and reusing existing infrastructure is itself a review criterion (ch 5). + +```r +# Validation - BiocCheck is authoritative +BiocCheck::BiocCheckGitClone() +BiocCheck::BiocCheck('new-package' = TRUE) + +# Scaffolding - biocthis writes Bioconductor-shaped files +biocthis::use_bioc_description(biocViews = "Software, ") +biocthis::use_bioc_news_md() +biocthis::use_bioc_vignette(name = "", title = "Introduction to ") +biocthis::use_bioc_citation() +biocthis::use_bioc_github_action() +``` + +`use_bioc_description()` writes a **fresh** DESCRIPTION; it does not merge into an existing one. +Internally it calls `usethis::use_description()`, which calls `write_over()`, which asks before +replacing an existing file - and in a non-interactive session it declines silently. So for a +package that already has a DESCRIPTION, this call very often does nothing at all and you get no +error. Add `biocViews` by hand instead, or approve the overwrite knowing it discards the +DESCRIPTION you have. Everything else in the chain appends and is safe on an existing package. + +`biocViews = "Software"` on its own is a BiocCheck **ERROR**: "Add biocViews other than Software". +The top-level terms (Software, AnnotationData, ExperimentData, Workflow) do not count on their +own - pick specific terms from the vocabulary at +https://bioconductor.org/packages/release/BiocViews.html, e.g. +`"Software, GeneExpression, Transcriptomics"`. Two other things BiocCheck flags on a freshly +scaffolded package: the placeholder Description is "too concise" (it wants at least three +sentences), and a Software package with no Bioconductor dependencies gets a warning suggesting +CRAN instead. + +`use_bioc_citation()` leaves `inst/CITATION` unfinished, and unfinished here means broken. The +template substitutes `{{Title}}` and `{{github_owner}}`; the function passes neither a `Title` +nor - on any package that has no GitHub remote configured yet - an owner. The file lands with an +empty title and an empty author, `utils::citation()` errors on either ("a bibentry of bibtype +'Manual' has to specify the field: title"), and because the generated vignette calls `citation()`, +`R CMD build` fails at "creating vignettes". Verified against biocthis 1.23.0 on 2026-08-14. Fill +in the title, the author, and the placeholder `10.1101/TODO` DOI before building anything. + +Install with: + +```r +BiocManager::install(c( + "BiocCheck", "biocthis", + # use_bioc_vignette() adds these to Suggests and refuses to run unless they are installed + "BiocStyle", "knitr", "RefManageR", "sessioninfo", "testthat" +)) +``` + +BiocCheck cannot measure the two +timing gate items (`R CMD check --no-build-vignettes` under 10 min, under 8 GB memory) - those +need a real build. + +Current cycle: Bioconductor release 3.23, devel 3.24, both on R 4.6.0. Build against devel for a +new submission. Never guess this pair - it changes twice a year, `knowledge/SOURCES.md` records +what was verified and when, and https://bioconductor.org/config.yaml is authoritative. + +Run the two BiocCheck calls via Bash from the package root when BiocCheck is installed, and parse +the output into your findings. When BiocCheck is unavailable, mark those gate items "not run - +BiocCheck unavailable" and audit statically from the files instead. Report the two timing items as +"requires a build" rather than claiming a verdict on them; do not run `R CMD check` yourself unless +the user asks, since a full check can take many minutes. Never guess a gate result you did not +measure. + +## Report format +Return a structured report, most severe first: + +- Blockers (must fix before submission): each as `- [file or check] problem. Fix: ... (ch N)`. +- Warnings (likely to draw reviewer requests): same format. +- Suggestions (nice to have): same format. +- Gate summary: a short pass/fail list of the gate items above. + +End with a one-line verdict: "Submission-ready" only if there are no blockers and the gate +passes; otherwise "Not yet - N blockers". diff --git a/docs/REFRESH.md b/docs/REFRESH.md new file mode 100644 index 0000000..0c9e450 --- /dev/null +++ b/docs/REFRESH.md @@ -0,0 +1,177 @@ +# Refresh runbook + +How to keep this repo in sync with the upstream projects it depends on. Run it when Bioconductor +cuts a release (around April and October), when the weekly `fidelity` CI job opens a drift issue, +or before any plugin version bump. + +Pinned baseline state lives in `knowledge/SOURCES.md`. This file is the procedure; that file is the +data. Both ship with the plugin. + +## Tracked upstreams + +Five, not one. A change to any of them can silently invalidate the guidance. + +| Upstream | How to detect a change | What it invalidates | +|---|---|---| +| `Bioconductor/pkgrevdocs` | commits API `sha` vs the pin; guide TOC vs the slug map | all of `knowledge/` | +| `Bioconductor/Contributions` `issue_template.md` | commits API for that path vs the pin | the pre-submission gate in `AGENTS.md`, `SKILL.md`, `agents/bioc-package-review.md` | +| `Bioconductor/BiocCheck` | release version + NEWS | what "BiocCheck clean" means; what the review agent should pre-empt | +| `lcolladotor/biocthis` | release version; the `use_bioc_*()` function list | the scaffolding commands the agent recommends, and `scripts/golden-path.R` | +| `grimbough/bioc-actions` | latest tag; each action's `action.yml` inputs | `.github/workflows/verify.yml` | + +Most of this is automated. `python3 scripts/verify.py --network` performs every detection step +below and, in CI, opens or updates a single tracking issue. Run it first; this runbook is what you +do with the result. + +## Priority order + +When more than one signal fires, work top-down. The ordering is by blast radius, not by effort. + +1. **Numeric or modal audit failure** - a specific claim in `knowledge/` is now factually wrong and + is being served to users. Fix immediately and restamp the file. +2. **A chapter URL 404s or redirects** - a chapter was renamed or removed, so the slug map is wrong. + Fix `knowledge/SOURCES.md` before touching content; every later step depends on the map. +3. **A mapped `.Rmd` changed** - those specific summaries are stale. Scoped refresh only. +4. **Pin moved but no mapped chapter changed** - cosmetic upstream change. Bump the pin, no content + work. + +## 1. pkgrevdocs (the guide) + +### Detect + +- Current commit: `https://api.github.com/repos/Bioconductor/pkgrevdocs/commits/devel`, field `sha`. + Compare against the pinned SHA in `knowledge/SOURCES.md`. Equal means stop. +- Changed files: `https://github.com/Bioconductor/pkgrevdocs/compare/...devel` +- Chapter set: compare the guide TOC (`https://contributions.bioconductor.org/index.html`) against + the slug map in `knowledge/SOURCES.md`. This is the only way to notice an **added** chapter - + every existing file still checks out, so no per-file check will see it. + +### Map changes to files + +For each changed chapter slug, look up its target in the `knowledge/SOURCES.md` map. Only those +files need regenerating. + +- New chapter: new summary file, plus a router entry in `AGENTS.md`, plus a map entry in + `knowledge/SOURCES.md`. +- Removed chapter: delete its section and fix inbound references. +- Renamed chapter: update the slug map and the `Source:` footer of the affected file. + +### Regenerate + +Re-run the summary pass for the affected files only. Format rules that must hold, since +`scripts/verify.py` enforces them: + +- Plain markdown. No emoji anywhere. +- Task-oriented - actionable rules, thresholds, required and forbidden patterns. Not a + transcription of the chapter. +- Roughly 40-120 lines per file. +- Ends with a `Source:` line carrying the canonical chapter URL, and a `Fetched ` stamp. +- **Preserve upstream modality.** If the guide says "should" or "ideally", the summary says + "should" or "ideally". Hardening a recommendation into a requirement is a defect, and the + modal-verb audit in `verify.py --network` will fail on it. +- Restamp `Fetched` on every file you touch. `verify.py` fails when a file's last git commit is + newer than its stamp, so an edited-but-unstamped file is caught. + +If the gate numbers, the version rule, or the submission mechanics changed, also update the inline +copies in `AGENTS.md`, `skills/bioconductor-package-dev/SKILL.md` and +`agents/bioc-package-review.md`. These repeat the gate deliberately, and `verify.py` requires the +copies to stay identical. + +## 2. Contributions issue_template.md (the authoritative gate) + +The tracker's issue template is the checklist a submitter actually ticks. It, not our prose, is the +authority on what submission requires. + +- Detect: `https://api.github.com/repos/Bioconductor/Contributions/commits?path=issue_template.md&per_page=1` +- Content: `https://raw.githubusercontent.com/Bioconductor/Contributions/devel/issue_template.md` + +If a checkbox is added, removed, or reworded, update the gate in all three router files and in +`knowledge/01-submissions.md`. This template changes rarely - the pinned commit dates to 2021 - so +any movement is worth reading in full rather than skimming a diff. + +## 3. BiocCheck (the validator) + +This repo delegates all validation to BiocCheck rather than reimplementing it, so BiocCheck's +behavior is part of our contract with users. + +- Detect: release version at `https://bioconductor.org/packages/release/bioc/html/BiocCheck.html`; + changes at `https://raw.githubusercontent.com/Bioconductor/BiocCheck/devel/NEWS`. + +New or removed checks change what "BiocCheck clean" means. If a check is added that maps to advice +we give, `agents/bioc-package-review.md` should pre-empt it so users hear it from the agent before +they hear it from the tool. If a check is removed, drop any advice that existed only to satisfy it. + +The two gate items BiocCheck cannot measure are the timing ones - `R CMD check --no-build-vignettes` +under 10 minutes, and under 8 GB memory. Those need a real build; CI measures them. + +## 4. biocthis (the scaffolder) + +This repo recommends biocthis for scaffolding rather than shipping competing templates. The +commands we tell users to run must exist and must still produce an acceptable package. + +- Detect: release version at `https://bioconductor.org/packages/release/bioc/html/biocthis.html`; + function list at `https://api.github.com/repos/lcolladotor/biocthis/contents/R`. + +If a `use_bioc_*()` function is renamed or removed, the command block in `AGENTS.md`, `SKILL.md` +and `agents/bioc-package-review.md` is wrong, and `scripts/golden-path.R` will fail in CI. Those +four places carry the same block and `verify.py` requires them identical, so fix all four together. + +Note `biocthis_example_pkg()` is not a Bioconductor-ready generator - it wraps +`usethis::create_package()` in `tempdir()` and produces a bare skeleton. The Bioc-ready path is the +`use_bioc_*()` chain, which is what `golden-path.R` runs. + +## 5. bioc-actions (the CI harness) + +- Detect: `https://api.github.com/repos/grimbough/bioc-actions/tags`; input schemas at + `https://raw.githubusercontent.com/grimbough/bioc-actions///action.yml` for + `setup-bioc`, `build-install-check`, `run-BiocCheck`, `use-bioc-caches`. + +Pin a tag in `.github/workflows/verify.yml`, never a branch. A changed input name is a red build +with a confusing message; reading the `action.yml` diff first saves the debugging. +`scripts/verify.py` fails if the tag in the workflow and the tag in `knowledge/SOURCES.md` differ. + +Only three of the four actions are used: `setup-bioc`, `build-install-check`, `run-BiocCheck`. +`use-bioc-caches` is deliberately excluded because it pins `actions/cache@v2`, which GitHub +auto-fails - the job dies in "Set up job" with no step ever running, and the error names the +deprecated cache rather than the action that pulled it in. Broken at `v1.0.16` and on `main` as +of 2026-08-14. When bumping the pin, check whether that has been fixed; if it has, the workflow +can drop its hand-rolled `actions/cache@v4` step. + +## 6. Update the baseline + +In `knowledge/SOURCES.md`, set every pin you verified: the pkgrevdocs SHA and commit date, the +Contributions issue_template SHA, the BiocCheck and biocthis versions, the bioc-actions tag, and +the "Summaries fetched" date. + +## 7. Verify + +Run in this order; each is cheaper than the next and catches different failures. + +1. `python3 scripts/verify.py` - static checks. Must be clean. +2. `python3 scripts/verify.py --network` - drift, URL liveness, numeric and modal audits, gate vs + tracker, chapter coverage. Must be clean. +3. `PATH=/opt/homebrew/bin:$PATH claude plugin validate .` - manifests. Expect "passed". +4. `Rscript scripts/golden-path.R` then `R CMD build` and `BiocCheck` on the result - confirms the + scaffolding commands we recommend still produce an acceptable package. CI does this too, but + running it locally first means the first CI run is not the first execution. +5. `claude --plugin-dir . -p "..."` - load test; confirm skill and agent load and the changed + content is reflected. +6. `claude plugin eval . --ablation with-without` - behavioral cases. Costs tokens; run when the + `description` fields or the routing changed, not on every refresh. + +## 8. Release + +- Bump `version` in `.claude-plugin/plugin.json`. Plugin users only receive updates on a bump. +- Note the change in the commit message. +- Commit and push to `github.com/ybaeus/bioc_package_dev`. Marketplace users then run + `/plugin marketplace update` followed by `/plugin update bioconductor-package-dev`. + +## Notes + +- The canonical chapter link in each summary is always authoritative. A lagging summary still points + readers at the correct source, which is why the `Source:` footer is mandatory. +- Never edit a `knowledge/` file without updating its `Fetched` stamp in the same commit. +- Environment: `env -u CURL_CA_BUNDLE curl -sS ` fetches content fine; the + `CURL_CA_BUNDLE` variable is set to a stale path and unsetting it is sufficient. `-k` is not + needed and should not be used. R 4.4.0 at `/usr/local/bin`; `claude` at `/opt/homebrew/bin`; + `gh` is not installed locally, so use the GitHub REST API via curl. diff --git a/evals/README.md b/evals/README.md new file mode 100644 index 0000000..2ac3e58 --- /dev/null +++ b/evals/README.md @@ -0,0 +1,64 @@ +# Behavioral evals + +Layer 4 of the verification described in the top-level README. The other three layers check what +the files say; these check what the plugin does. + +``` +claude plugin eval . --ablation with-without --scaffold +claude plugin eval . --case trigger-conversion --verbose +``` + +`--scaffold` is required for the two agent cases: it runs each case's `scaffold_script`, which +builds the deliberately broken fixture package they audit. The flag is off by default because a +`scaffold_script` is author-supplied shell; these were authored here. + +`--ablation with-without` adds a no-plugin baseline arm and reports the delta. That delta is the +evidence, not the absolute score - a model can answer "what version for a new submission" from +memory, and a case that passes equally well without the plugin is a case that is not measuring +this plugin. + +## Why these cases + +| Case | What breaks if it fails | +|---|---| +| `trigger-conversion` | the skill no longer fires for the audience it was written for | +| `trigger-cran-move` | someone is told they can be on CRAN and Bioconductor at once | +| `trigger-implicit` | the skill only works when the user already knows the word "Bioconductor" | +| `anti-trigger` | the skill fires on everything, spending context and dragging Bioconductor rules into CRAN answers | +| `facts-version` | the single most asked question gets a plausible wrong answer | +| `router-large-data` | large data advice stops at "compress it" instead of ExperimentHub | +| `style-bioc` | generated code uses tidyverse habits and `1:n`, which BiocCheck flags | +| `workflow-after-submission` | the post-submission sequence is missing or out of order | +| `agent-review` | the review agent misses planted defects, or invents ones that are not there | +| `agent-verdict` | the agent hedges instead of answering, or reports a recommendation as a blocker | + +The trigger cases are the highest-value three. The `description` field in `SKILL.md` is the +single point of failure that can make the whole plugin inert while every static check still +passes, and nothing but a behavioral eval can see it. + +## How the graders are built + +Deterministic regex wherever the claim is checkable that way, which is most of them and costs +nothing. LLM graders only on the two agent cases, where the thing being judged is whether a +report is accurate and correctly scoped - and there the criteria name the specific findings that +count and the specific false positives that do not, because a vague criterion just moves the +judgement somewhere it cannot be inspected. + +The agent cases grade the content of the answer rather than asserting that the subagent was +spawned. Whether the work happens in the main loop or in `bioc-package-review`, the user-visible +requirement is the same: find the real defects, invent none, and do not claim a check result that +was never measured. + +## Keeping this in step with the README + +Every prompt in the top-level README's "Example prompts" section appears verbatim as some case's +`execution.prompt`. `scripts/verify.py` enforces it, so a documented prompt is always one CI has +an opinion about, and editing either side without the other fails the static check. + +## Not yet executed + +`claude plugin eval` is early access and was not available in the CLI on the machine where these +were written, so the cases are authored against the schema the CLI validates against but have +never been run. Expect the first run to need adjustment - most likely to the regex graders, which +assert on phrasing rather than on meaning. The evals job is dispatch-only, so a broken case here +cannot break anything else. diff --git a/evals/agent-review/case.yaml b/evals/agent-review/case.yaml new file mode 100644 index 0000000..601b0a1 --- /dev/null +++ b/evals/agent-review/case.yaml @@ -0,0 +1,98 @@ +schema_version: "1.0" +name: agent-review +description: >- + Audits a package with exactly three planted defects: Version 0.1.0 instead of 0.99.0, no + biocViews field, and no vignette. Everything else is deliberately fine, so a report that finds + the three and does not invent a fourth is the pass condition. The fixture is built with shell + only - no R, no biocthis - so this case runs anywhere and cannot fail for an unrelated reason. +tags: [agent, review] +plugins: [bioconductor-package-dev] +runs: 2 + +context: + scaffold_script: | + set -euo pipefail + rm -rf BrokenPkg + mkdir -p BrokenPkg/R BrokenPkg/man BrokenPkg/tests/testthat + cat > BrokenPkg/DESCRIPTION <<'EOF' + Package: BrokenPkg + Type: Package + Title: Summarise Counts Across Samples + Version: 0.1.0 + Authors@R: person("Ada", "Lovelace", email = "ada@example.org", role = c("aut", "cre")) + Description: Summarises count matrices across samples for downstream analysis. + License: Artistic-2.0 + Encoding: UTF-8 + Imports: SummarizedExperiment + Suggests: testthat + RoxygenNote: 7.3.2 + EOF + cat > BrokenPkg/NAMESPACE <<'EOF' + export(summariseCounts) + importFrom(SummarizedExperiment,assay) + EOF + cat > BrokenPkg/R/summariseCounts.R <<'EOF' + #' Summarise counts across samples + #' + #' @param se A SummarizedExperiment. + #' @return A numeric vector of column sums. + #' @export + summariseCounts <- function(se) { + counts <- SummarizedExperiment::assay(se) + colSums(counts) + } + EOF + cat > BrokenPkg/man/summariseCounts.Rd <<'EOF' + \name{summariseCounts} + \alias{summariseCounts} + \title{Summarise counts across samples} + \usage{summariseCounts(se)} + \arguments{\item{se}{A SummarizedExperiment.}} + \value{A numeric vector of column sums.} + \description{Summarise counts across samples.} + \examples{ + library(SummarizedExperiment) + se <- SummarizedExperiment(assays = list(counts = matrix(1:4, nrow = 2))) + summariseCounts(se) + } + EOF + cat > BrokenPkg/tests/testthat.R <<'EOF' + library(testthat) + library(BrokenPkg) + test_check("BrokenPkg") + EOF + cat > BrokenPkg/tests/testthat/test-summariseCounts.R <<'EOF' + test_that("summariseCounts returns one value per column", { + expect_length(summariseCounts(se), 2L) + }) + EOF + +execution: + prompt: >- + Audit my package for Bioconductor submission readiness. The package is in the BrokenPkg + directory. + max_turns: 25 + timeout_seconds: 900 + allowed_tools: [Read, Grep, Glob] + +graders: + - type: regex + name: finds-the-version + pattern: "0\\.99\\.0" + - type: regex + name: finds-the-missing-biocviews + pattern: "biocViews" + - type: regex + name: finds-the-missing-vignette + pattern: "vignette" + flags: "i" + - type: llm + name: report-is-accurate-and-scoped + criteria: >- + The response reports all three real problems with the package: the Version field is 0.1.0 + rather than 0.99.0, the DESCRIPTION has no biocViews field, and there is no vignette. It + must not claim the package is ready to submit. It must not report problems that do not + exist - the package does have a NAMESPACE with explicit exports, a man page with a runnable + example, and unit tests under tests/, so claiming any of those is missing is a failure. If + it reports the check or BiocCheck gate items, it says they were not run rather than + asserting a result it did not measure. diff --git a/evals/agent-verdict/case.yaml b/evals/agent-verdict/case.yaml new file mode 100644 index 0000000..1c62499 --- /dev/null +++ b/evals/agent-verdict/case.yaml @@ -0,0 +1,95 @@ +schema_version: "1.0" +name: agent-verdict +description: >- + Same fixture as agent-review, different question. This one tests the part that is easy to get + wrong in the pleasant direction: asked whether a package would pass, the answer has to be no, + and it has to preserve the two tiers - a wrong version is a warning, a missing biocViews and a + missing vignette are blockers. Encouraging vagueness here is the failure mode. +tags: [agent, review, verdict] +plugins: [bioconductor-package-dev] +runs: 2 + +context: + scaffold_script: | + set -euo pipefail + rm -rf BrokenPkg + mkdir -p BrokenPkg/R BrokenPkg/man BrokenPkg/tests/testthat + cat > BrokenPkg/DESCRIPTION <<'EOF' + Package: BrokenPkg + Type: Package + Title: Summarise Counts Across Samples + Version: 0.1.0 + Authors@R: person("Ada", "Lovelace", email = "ada@example.org", role = c("aut", "cre")) + Description: Summarises count matrices across samples for downstream analysis. + License: Artistic-2.0 + Encoding: UTF-8 + Imports: SummarizedExperiment + Suggests: testthat + RoxygenNote: 7.3.2 + EOF + cat > BrokenPkg/NAMESPACE <<'EOF' + export(summariseCounts) + importFrom(SummarizedExperiment,assay) + EOF + cat > BrokenPkg/R/summariseCounts.R <<'EOF' + #' Summarise counts across samples + #' + #' @param se A SummarizedExperiment. + #' @return A numeric vector of column sums. + #' @export + summariseCounts <- function(se) { + counts <- SummarizedExperiment::assay(se) + colSums(counts) + } + EOF + cat > BrokenPkg/man/summariseCounts.Rd <<'EOF' + \name{summariseCounts} + \alias{summariseCounts} + \title{Summarise counts across samples} + \usage{summariseCounts(se)} + \arguments{\item{se}{A SummarizedExperiment.}} + \value{A numeric vector of column sums.} + \description{Summarise counts across samples.} + \examples{ + library(SummarizedExperiment) + se <- SummarizedExperiment(assays = list(counts = matrix(1:4, nrow = 2))) + summariseCounts(se) + } + EOF + cat > BrokenPkg/tests/testthat.R <<'EOF' + library(testthat) + library(BrokenPkg) + test_check("BrokenPkg") + EOF + cat > BrokenPkg/tests/testthat/test-summariseCounts.R <<'EOF' + test_that("summariseCounts returns one value per column", { + expect_length(summariseCounts(se), 2L) + }) + EOF + +execution: + prompt: >- + Would this package pass review? Tell me what a reviewer would flag. The package is in the + BrokenPkg directory. + max_turns: 25 + timeout_seconds: 900 + allowed_tools: [Read, Grep, Glob] + +graders: + - type: regex + name: does-not-green-light-it + pattern: "(ready to submit|submission-ready|good to go|ready for submission)" + flags: "i" + match: not_contains + - type: regex + name: names-the-blockers + pattern: "biocViews" + - type: llm + name: verdict-is-negative-and-tiered + criteria: >- + The response answers no, this package would not pass as it stands, and says so plainly + rather than hedging. It names the missing biocViews field and the missing vignette as + things that must be fixed, and the Version being 0.1.0 rather than 0.99.0 as something to + change. Credit a response that distinguishes hard requirements from recommendations; + penalise one that presents the source build size, check duration, or memory limits as + requirements, since upstream states those as "should" and "recommended". diff --git a/evals/anti-trigger/case.yaml b/evals/anti-trigger/case.yaml new file mode 100644 index 0000000..b88c619 --- /dev/null +++ b/evals/anti-trigger/case.yaml @@ -0,0 +1,37 @@ +schema_version: "1.0" +name: anti-trigger +description: >- + A plain CRAN question with no Bioconductor content. A skill that fires on everything is as + broken as one that fires on nothing - it burns context and drags Bioconductor rules into + answers where they do not apply. Three runs because false positives are intermittent. +tags: [trigger, negative] +plugins: [bioconductor-package-dev] +runs: 3 + +execution: + prompt: >- + I am writing a tidyverse-style R package for CRAN. How do I set up testthat and get my first + test running? + max_turns: 6 + timeout_seconds: 300 + +graders: + # Deliberately not a ban on the word "Bioconductor". A local smoke test showed the correct + # behaviour is to say "not Bioconductor, skipping that guidance" and then answer the CRAN + # question - which a blanket ban would have scored as a failure. What must not appear is + # substantive Bioconductor guidance. + - type: regex + name: does-not-impose-the-submission-version + pattern: "0\\.99\\.0" + match: not_contains + - type: regex + name: does-not-drag-in-bioc-metadata + pattern: "(biocViews|BiocCheck|BiocManager)" + match: not_contains + - type: regex + name: does-not-mention-the-tracker + pattern: "Contributions" + match: not_contains + - type: regex + name: actually-answers-the-question + pattern: "testthat" diff --git a/evals/facts-version/case.yaml b/evals/facts-version/case.yaml new file mode 100644 index 0000000..b2c2244 --- /dev/null +++ b/evals/facts-version/case.yaml @@ -0,0 +1,23 @@ +schema_version: "1.0" +name: facts-version +description: >- + The single most asked question, and one where a plausible-sounding wrong answer (1.0.0, or the + package's existing version) costs a review cycle. Also checks the answer explains the devel or + release parity rule rather than just emitting a number. +tags: [facts] +plugins: [bioconductor-package-dev] +runs: 2 + +execution: + prompt: "What version number do I use for a new submission?" + max_turns: 4 + timeout_seconds: 300 + +graders: + - type: regex + name: gives-the-right-version + pattern: "0\\.99\\.0" + - type: regex + name: explains-the-scheme + pattern: "(odd|devel|x\\.y\\.z)" + flags: "i" diff --git a/evals/router-large-data/case.yaml b/evals/router-large-data/case.yaml new file mode 100644 index 0000000..a33874d --- /dev/null +++ b/evals/router-large-data/case.yaml @@ -0,0 +1,22 @@ +schema_version: "1.0" +name: router-large-data +description: >- + Tests the router rather than a single fact: 300 MB is far past the size limits, so the answer + has to leave the package and land on ExperimentHub or AnnotationHub. Answering "compress it" + would be confidently useless. +tags: [router, facts] +plugins: [bioconductor-package-dev] +runs: 2 + +execution: + prompt: "I have 300 MB of reference data, where does it go?" + max_turns: 6 + timeout_seconds: 300 + +graders: + - type: regex + name: routes-to-a-hub + pattern: "(ExperimentHub|AnnotationHub)" + - type: regex + name: mentions-the-size-limit + pattern: "(5 ?MB|10 ?MB)" diff --git a/evals/style-bioc/case.yaml b/evals/style-bioc/case.yaml new file mode 100644 index 0000000..45cb3f4 --- /dev/null +++ b/evals/style-bioc/case.yaml @@ -0,0 +1,26 @@ +schema_version: "1.0" +name: style-bioc +description: >- + Bioconductor style differs from the tidyverse defaults a model reaches for: assignment arrow, + four-space indent, and seq_len/seq_along instead of 1:n. The 1:n habit is not cosmetic - it + iterates backwards when the length is zero - and BiocCheck flags it. +tags: [style] +plugins: [bioconductor-package-dev] +runs: 2 + +execution: + prompt: "Write me a function that iterates over samples" + max_turns: 6 + timeout_seconds: 300 + +graders: + - type: regex + name: uses-assignment-arrow + pattern: "<-" + - type: regex + name: uses-safe-sequence + pattern: "(seq_len|seq_along)" + - type: regex + name: avoids-one-colon-n + pattern: "1:(n|length\\(|ncol\\(|nrow\\()" + match: not_contains diff --git a/evals/trigger-conversion/case.yaml b/evals/trigger-conversion/case.yaml new file mode 100644 index 0000000..4e2c81f --- /dev/null +++ b/evals/trigger-conversion/case.yaml @@ -0,0 +1,26 @@ +schema_version: "1.0" +name: trigger-conversion +description: >- + The main case this plugin exists for: someone with a working package on GitHub who wants it in + Bioconductor. If the skill description stops covering conversion vocabulary the plugin goes + inert here while every static check still passes green, so this is the highest-value case in + the suite. +tags: [trigger, conversion] +plugins: [bioconductor-package-dev] +runs: 3 + +execution: + prompt: "I have an R package on GitHub, what do I need to do to submit it to Bioconductor?" + max_turns: 6 + timeout_seconds: 300 + +graders: + - type: regex + name: names-the-starting-version + pattern: "0\\.99\\.0" + - type: regex + name: names-the-tracker + pattern: "Contributions" + - type: regex + name: names-the-validator + pattern: "BiocCheck" diff --git a/evals/trigger-cran-move/case.yaml b/evals/trigger-cran-move/case.yaml new file mode 100644 index 0000000..4f47368 --- /dev/null +++ b/evals/trigger-cran-move/case.yaml @@ -0,0 +1,23 @@ +schema_version: "1.0" +name: trigger-cran-move +description: >- + CRAN and Bioconductor are mutually exclusive - "a package can only be submitted to one or the + other". Answering this one wrong sends someone down a path that ends in a rejected submission, + so the answer has to be unambiguous rather than encouraging. +tags: [trigger, conversion, facts] +plugins: [bioconductor-package-dev] +runs: 2 + +execution: + prompt: "My package is on CRAN, can I move it to Bioconductor?" + max_turns: 6 + timeout_seconds: 300 + +graders: + - type: regex + name: says-not-both + pattern: "(one or the other|cannot be (on|in) both|not be (on|in) both|only one of)" + flags: "i" + - type: regex + name: mentions-cran + pattern: "CRAN" diff --git a/evals/trigger-implicit/case.yaml b/evals/trigger-implicit/case.yaml new file mode 100644 index 0000000..c23c834 --- /dev/null +++ b/evals/trigger-implicit/case.yaml @@ -0,0 +1,22 @@ +schema_version: "1.0" +name: trigger-implicit +description: >- + The word "Bioconductor" never appears in the prompt, but biocViews is a Bioconductor-only + field. AGENTS.md and the skill description both claim to fire when the package clearly targets + Bioconductor without the user saying so; this is what tests that claim. +tags: [trigger, implicit] +plugins: [bioconductor-package-dev] +runs: 3 + +execution: + prompt: "My DESCRIPTION has no biocViews - what do I put there?" + max_turns: 6 + timeout_seconds: 300 + +graders: + - type: regex + name: names-a-real-biocviews-term + pattern: "(Software|ExperimentData|AnnotationData|Workflow)" + - type: regex + name: keeps-the-field-name + pattern: "biocViews" diff --git a/evals/trigger-scripts/case.yaml b/evals/trigger-scripts/case.yaml new file mode 100644 index 0000000..a2a9d9e --- /dev/null +++ b/evals/trigger-scripts/case.yaml @@ -0,0 +1,67 @@ +schema_version: "1.0" +name: trigger-scripts +description: >- + The starting point one step earlier than trigger-conversion: not a package yet, just analysis + code. The advice diverges here - scaffolding is the right move, where for an existing package it + is the wrong one - so an answer that treats scripts like a package is a failure even if every + fact in it is true. +tags: [trigger, conversion, scripts] +plugins: [bioconductor-package-dev] +runs: 3 + +context: + scaffold_script: | + set -euo pipefail + rm -rf crufty + mkdir -p crufty + cat > crufty/analysis.R <<'EOF' + setwd("/Users/me/projects/rnaseq") + install.packages("ggplot2") + library(ggplot2) + rm(list = ls()) + + counts <- read.csv("/Users/me/projects/rnaseq/counts.csv", row.names = 1) + + res <- c() + for (i in 1:ncol(counts)) { + res[i] = sum(counts[, i] > 0) + } + + plot(res) + EOF + cat > crufty/helpers.R <<'EOF' + normalise = function(m) { + t(t(m) / colSums(m)) + } + EOF + +execution: + prompt: >- + I have this crufty analysis code, review it and make it into a Bioconductor-submittable + package. It is in the crufty directory. + max_turns: 25 + timeout_seconds: 900 + allowed_tools: [Read, Grep, Glob] + +graders: + - type: regex + name: says-it-is-not-a-package-yet + pattern: "(not (yet )?a package|no DESCRIPTION|needs to become a package)" + flags: "i" + - type: regex + name: names-the-load-time-side-effects + pattern: "(setwd|rm\\(list|install\\.packages)" + - type: regex + name: gives-the-starting-version + pattern: "0\\.99\\.0" + - type: llm + name: scaffolds-rather-than-audits + criteria: >- + The response recognises that this is analysis code and not a package yet, and that the first + move is to create a package around it rather than to audit it against the submission gate. + It should flag the load-time side effects (setwd, rm(list = ls()), install.packages, the + hardcoded absolute paths) as things that cannot survive into a package, and should say the + script body needs to become exported, documented functions. Credit raising the question of + whether this is a Software package or a Workflow package. Penalise a response that reports a + list of gate blockers as though a package existed, and penalise one that claims the code is + close to submittable. diff --git a/evals/workflow-after-submission/case.yaml b/evals/workflow-after-submission/case.yaml new file mode 100644 index 0000000..a56fdc4 --- /dev/null +++ b/evals/workflow-after-submission/case.yaml @@ -0,0 +1,27 @@ +schema_version: "1.0" +name: workflow-after-submission +description: >- + What happens after the issue is opened is the part submitters have no way to guess: an + automated build fires first, a human reviewer comes later, and every fix needs a version bump + to propagate. Tests that workflow.md is reachable and ordered, not just present. +tags: [router, workflow] +plugins: [bioconductor-package-dev] +runs: 2 + +execution: + prompt: "Walk me through what happens after I open the Contributions issue." + max_turns: 6 + timeout_seconds: 300 + +graders: + - type: regex + name: names-the-builder + pattern: "(Single Package Builder|SPB)" + - type: regex + name: mentions-review + pattern: "review" + flags: "i" + - type: regex + name: mentions-the-version-bump + pattern: "(bump|increment)" + flags: "i" diff --git a/knowledge/01-submissions.md b/knowledge/01-submissions.md new file mode 100644 index 0000000..deaffa4 --- /dev/null +++ b/knowledge/01-submissions.md @@ -0,0 +1,101 @@ +# Covers: Chapter 1 - Bioconductor package submission overview, eligibility, package types, and submission mechanics. + +## Eligibility + +Upstream states this list as "To submit a package to Bioconductor the package **should**" - these +are review criteria, not a mechanical pass/fail gate. Some individual items carry harder modality +than the list stem, and that difference is preserved below. + +- Should address areas of high-throughput genomic analysis (sequencing, expression and other + microarrays, flow cytometry, mass spectrometry, image analysis); see biocViews. +- Should interoperate with other Bioconductor packages by re-using common data structures and + existing infrastructure (e.g. `rtracklayer::import()` for common genomic file input) rather + than reinventing them. +- Should adopt software best practices enabling reproducible research: full documentation and + fully evaluated vignettes, plus commitment to long-term user support on the support site. +- **Cannot** exist on CRAN - "A package can only be submitted to one or the other." +- **Cannot** depend on any package, or version of a package, not yet available on CRAN or + Bioconductor; it should work with the current publicly available version. +- Should comply with the Package Guidelines. + +Note on scope: the genomic-analysis criterion is a "should", and Bioconductor does host accepted +Software packages that perform no genomic analysis themselves (BiocCheck, biocthis, BiocStyle are +developer infrastructure). A tool outside classic genomics is therefore a judgement call for the +reviewers, not an automatic rejection - but the burden is on the submitter to argue the fit, and +this is a good thing to raise in the submission issue rather than discover during review. + +## Package types + +- Software: algorithms, resource access, analysis, visualization. Most common + submission type. Goes through the Contributions issue tracker. +- Experiment Data: curated datasets for examples/vignettes. Typically a single + dataset; for larger files use ExperimentHub. Do NOT open a separate issue - + add the experiment data package to the SAME issue as its software package. +- Annotation: databases mapping identifiers to information; updated every 6 + months. Prefer AnnotationHub when possible. Do NOT use the tracker - instead + email packages@bioconductor.org. +- Workflow: demonstrate a multi-package bioinformatics workflow. No man/, R/, or + data/ directories required. Follow the non-software development section. + +## Submission mechanics (Software / Experiment Data) + +- Host the package in a GitHub repository. +- The package must live on the repository's DEFAULT branch - you cannot specify an + alternative branch. Upstream: "The default branch must contain only package code. + Any files or directories for other applications (Github Actions, devtools, etc) + should be in a different branch." Note the two different strengths in those two + sentences: do not report a package as non-compliant solely for carrying a CI + workflow on the default branch. +- Package name should not conflict (case-insensitive) with any current or past + Bioconductor or CRAN package. The contributor grants Bioconductor rights to + the package name (CRAN-style naming/ownership policy applies). +- Open a NEW issue on the tracker at github.com/Bioconductor/Contributions with + the package name as the issue title; link the GitHub repo in the issue and + follow the tracker README.md guidelines. +- The submitter MUST be listed as the package maintainer in DESCRIPTION + (maintainer == submitter) - used to verify credentials. +- The Single Package Builder webhook auto-builds the package on submission (and + on each subsequent push); the build must PASS on all platforms before review + proceeds. +- Annotation packages: email packages@bioconductor.org instead of opening an + issue on the tracker. + +## Maintainer obligations (ongoing) + +- Follow Bioconductor guidelines: version numbering, coding style, performance, + and memory usage standards. +- Maintain the package with git version control. +- Monitor build reports (weekly to daily) and fix breakages promptly. +- Subscribe to the bioc-devel mailing list. +- Register on the support site and monitor the package's "Watched Tags". +- Respond promptly to bug reports and user questions. +- Keep the maintainer email in DESCRIPTION accurate and reachable. +- Bump the "z" (patch) version number on EVERY commit; without a version bump, + changes will not propagate to the build system. + +## Review timeline and response deadlines (hard numbers) + +- Full review typically takes 2 to 6 weeks. +- Expect progress (submitter updates or reviewer comments) within 2-3 weeks. +- After roughly 3-4 weeks of inactivity, reviewers MAY close the issue; respond + within the 2-3 week window to keep it open. +- Changes pushed to the devel branch appear in builds within about 24-48 hours. + +## Release cycle and post-acceptance + +- Two releases per year (approximately April and October). +- Accepted packages first enter the 'devel' branch. +- Bug fixes allowed in both devel and release branches; NEW features restricted + to the devel branch only. +- Annotation packages are updated every 6 months. +- On successful build, a landing page is auto-created and the package becomes + installable via BiocManager::install() (devel users first). + +## Getting help + +- General maintainer help: maintainer@bioconductor.org and the bioc-devel list. +- Topics: S4 class design, implementation guidance, code structure, + documentation review. + +Source: https://contributions.bioconductor.org/bioconductor-package-submissions.html (and overview: https://contributions.bioconductor.org/submission-overview.html) +Fetched 2026-08-14 from contributions.bioconductor.org (Bioconductor devel guide). diff --git a/knowledge/SOURCES.md b/knowledge/SOURCES.md new file mode 100644 index 0000000..04f8fa5 --- /dev/null +++ b/knowledge/SOURCES.md @@ -0,0 +1,116 @@ +# Sources and refresh baseline + +This file records where each summary came from and the exact upstream state it was generated +against. Use it to detect drift and to refresh only what changed. `docs/REFRESH.md` is the +procedure; this file is the data. `scripts/verify.py --network` reads both. + +## Tracked upstreams (update every pin on every refresh) + +Five upstreams, not one. A change to any of them can silently invalidate the guidance here. + +| Upstream | Pin | Verified | Invalidates | +|---|---|---|---| +| [Bioconductor/pkgrevdocs](https://github.com/Bioconductor/pkgrevdocs) (`devel`) | `9b078ea2a0ec05274be83cb12ea75473c6d0c808` (committed 2026-07-20) | 2026-08-14 | all of `knowledge/` | +| [Bioconductor/Contributions](https://github.com/Bioconductor/Contributions) `issue_template.md` | `d2631e3da63092937a96d476c6f7fb915a168069` (committed 2021-07-12) | 2026-08-14 | the pre-submission gate wording in `AGENTS.md`, `SKILL.md`, `agents/bioc-package-review.md` | +| [Bioconductor/BiocCheck](https://github.com/Bioconductor/BiocCheck) | release 1.48.1, devel 1.49.30 | 2026-08-14 | what "BiocCheck clean" means; what the review agent should pre-empt | +| [lcolladotor/biocthis](https://github.com/lcolladotor/biocthis) | release 1.22.0, devel 1.23.0 | 2026-08-14 | the scaffolding block in the three router files, and `scripts/golden-path.R` | +| [grimbough/bioc-actions](https://github.com/grimbough/bioc-actions) | `v1.0.16` (`455bb7a12b1f0df041fc1078de581d2c508839d9`); `setup-bioc`, `build-install-check` and `run-BiocCheck` only | 2026-08-14 | `.github/workflows/verify.yml` | + +Bioconductor cycle at the last refresh: Bioconductor release 3.23, devel 3.24, both on R 4.6.0 +(source: https://bioconductor.org/config.yaml). The same sentence appears in the three router +files, because an agent that is not told this triple will invent one; `verify.py --network` +checks all four against config.yaml. + +Other baseline facts: + +- Rendered guide: https://contributions.bioconductor.org +- Summaries fetched: 2026-08-14 (files edited on that date carry that `Fetched` stamp; files + untouched since the previous pass still carry 2026-07-23 and are still accurate, because the + pkgrevdocs pin has not moved between the two dates) + +Machine-readable endpoints used for drift detection: + +- pkgrevdocs commit: `https://api.github.com/repos/Bioconductor/pkgrevdocs/commits/devel`, field `sha` +- changed files since the pin: `https://github.com/Bioconductor/pkgrevdocs/compare/...devel` +- chapter order: `https://raw.githubusercontent.com/Bioconductor/pkgrevdocs/devel/_bookdown.yml` +- BiocCheck / biocthis versions: `https://bioconductor.org/packages/release/bioc/VIEWS` and + `.../devel/bioc/VIEWS` (parse `Package:` / `Version:` pairs - the HTML landing pages are not + reliably parseable) +- bioc-actions tags: `https://api.github.com/repos/grimbough/bioc-actions/tags` + +Known issue, verified 2026-08-14: `bioc-actions/use-bioc-caches` pins `actions/cache@v2`, which +GitHub auto-fails, killing the whole job during "Set up job" before any step runs. Broken at +`v1.0.16` and on the action's `main`, so a tag bump does not fix it. The workflow here uses +`actions/cache@v4` directly instead. Re-check on the next refresh; if upstream has moved to +cache v4, switching back is a simplification. Do not re-add the action without checking. + +## Chapter map: .Rmd source -> rendered slug -> knowledge file + +Two columns are needed, not one. The GitHub compare API reports changed **`.Rmd` filenames**, +while every `Source:` footer in `knowledge/` cites a **rendered slug** - and they routinely differ +(`package-maintainence.Rmd` renders to `package-maintenance.html`, note the upstream misspelling; +`bioc-classes-methods.Rmd` renders to `reusebioc.html`). A drift report can only be scoped to the +right files by joining through this table. + +Rendered pages live at `https://contributions.bioconductor.org/.html`. Chapter order and +`.Rmd` names come from `_bookdown.yml`; slugs come from each chapter's `{#id}` anchor. + +| .Rmd | Slug | Ch | Knowledge file | +|---|---|---|---| +| `index.Rmd` | `index` | - | not summarized (welcome page, no rules) | +| `package-submission.Rmd` | `submission-overview` | - | `01-submissions.md` | +| `package-submission.Rmd` | `bioconductor-package-submissions` | 1 | `01-submissions.md` | +| `devguide-introduction.Rmd` | `develop-overview` | - | not summarized (part overview, no rules) | +| `package-name.Rmd` | `package-name` | 2 | `development/package-name.md` | +| `general-package-development.Rmd` | `general` | 3 | `development/general-dev.md` | +| `important-bioc-features.Rmd` | `important-bioconductor-package-development-features` | 4 | `development/general-dev.md` | +| `bioc-classes-methods.Rmd` | `reusebioc` | 5 | `development/methods-classes.md` | +| `readme-file.Rmd` | `readme` | 6 | `development/metadata-files.md` | +| `description-file.Rmd` | `description` | 7 | `development/metadata-files.md` | +| `namespace-file.Rmd` | `namespace` | 8 | `development/metadata-files.md` | +| `news-file.Rmd` | `news` | 9 | `development/metadata-files.md` | +| `license-file.Rmd` | `license` | 10 | `development/metadata-files.md` | +| `citation-file.Rmd` | `citation` | 11 | `development/metadata-files.md` | +| `install-file.Rmd` | `sysdep` | 12 | `development/metadata-files.md` | +| `documentation.Rmd` | `docs` | 13 | `development/documentation.md` | +| `package-data.Rmd` | `data` | 14 | `development/data.md` | +| `unit-tests.Rmd` | `tests` | 15 | `development/tests.md` | +| `r-code.Rmd` | `r-code` | 16 | `development/r-code.md` | +| `fortran-C-python.Rmd` | `other-than-Rcode` | 17 | `development/compiled-thirdparty.md` | +| `shiny-apps.Rmd` | `shiny` | 18 | `development/shiny.md` | +| `ai-policy-third-party.Rmd` | `ai-policy-third-party` | 19 | `development/ai-policy.md` | +| `non-software-packages.Rmd` | `non-software` | 20 | `development/non-software-pkgs.md` | +| `gitignore-file.Rmd` | `gitignore` | 21 | `development/gitignore.md` | +| `build-check-bioccheck.Rmd` | `build-check-bioccheck` | 22 | `development/build-check-bioccheck.md` | +| `devguide-conclusion.Rmd` | `conclusion` | 23 | `development/build-check-bioccheck.md` | +| `package-maintainence.Rmd` | `package-maintenance` | - | not summarized (part overview, no rules) | +| `git-version-control.Rmd` | `git-version-control` | 24 | `maintenance.md` | +| `version-numbering.Rmd` | `versionnum` | 25 | `maintenance.md` | +| `troubleshoot-build-report.Rmd` | `troubleshooting-build-report` | 26 | `maintenance.md` | +| `debugging-c-code.Rmd` | `debugging-cc-code` | 27 | `maintenance.md` | +| `deprecation.Rmd` | `deprecation` | 28 | `maintenance.md` | +| `package-end-of-life.Rmd` | `package-end-of-life-policy` | 29 | `maintenance.md` | +| `branch-rename-faq.Rmd` | `branch-rename-faqs` | 30 | `maintenance.md` | +| `review-overview.Rmd` | `reviewer-resources-overview` | - | `reviewer.md` | +| `review-expectations.Rmd` | `review-expectation` | 31 | `reviewer.md` | +| `review-resources-and-tools.Rmd` | `reviewtools` | 32 | `reviewer.md` | +| `volunteer-to-review.Rmd` | `review-volunteer-chapter` | 33 | `reviewer.md` | +| `appendix.Rmd` | - | - | no page (part marker only) | +| `devel-branch.Rmd` | `use-devel` | A | `appendices.md` | +| `advanced-build-options.Rmd` | `advanced-build-options` | B | `appendices.md` | +| `web-query.Rmd` | `querying-web-resources` | C | `appendices.md` | +| `c-and-fortran.Rmd` | `c-fortran` | D | `appendices.md` | +| `mavericks.Rmd` | `cmavericks-best-practices` | E | `appendices.md` | +| `debug-rd-links.Rmd` | `man-links` | F | `appendices.md` | +| `news-for-bookdown.Rmd` | `booknews` | G | `appendices.md` | +| `references.Rmd` | `references-1` | H | `appendices.md` | + +Three slugs are deliberately marked "not summarized": `index`, `develop-overview` and +`package-maintenance` are welcome or part-overview pages whose entire body is a sentence or two of +orientation. They carry no rules to summarize. They are listed anyway so the chapter-coverage +check in `verify.py --network` can tell "we decided not to summarize this" apart from "we missed +a chapter". + +## Not chapter-derived (maintain by hand) + +`index.md` (router), `workflow.md` (runbook), and this file. diff --git a/knowledge/appendices.md b/knowledge/appendices.md new file mode 100644 index 0000000..edc4846 --- /dev/null +++ b/knowledge/appendices.md @@ -0,0 +1,64 @@ +# Appendices + +Covers: the key actionable point of each appendix (A-H) in the Bioconductor package development guide. + +## Appendix A - Using Devel Bioconductor + +Develop against Bioconductor devel so your package is ready when devel becomes the next release. The R version you pair with devel depends on the time of year (R releases once a year in mid-April; Bioconductor releases twice a year, mid-April and mid-October): + +- Mid-April to mid-October: use R-release (the current released R) with Bioconductor devel. +- Mid-October to mid-April: use R-devel (daily build) with Bioconductor devel, because a new R is coming in April. + +Rule of thumb: target the R version that users will have when the current devel branch becomes the release branch. + +Install/switch to devel: + +```r +if (!requireNamespace("BiocManager", quietly = TRUE)) + install.packages("BiocManager") +BiocManager::install(version = "devel") +BiocManager::valid() # check all packages are the correct devel versions +``` + +`BiocManager::install(version = "devel")` flips the active version to devel; `BiocManager::valid()` reports any out-of-date or "too new" packages. For the mid-October to mid-April window, first install R-devel (source from stat.ethz.ch/R/daily, macOS from mac.r-project.org, Windows rdevel from CRAN) and run the same commands in that R. + +## Appendix B - Advanced Build Options + +- Skip unsupported platforms via `Config/Bioconductor/UnsupportedPlatforms` in DESCRIPTION (or legacy `UnsupportedPlatforms:` in `.BBSoptions`); platforms are win, mac, etc. +- Long tests (>40 min): put them in a `longtests/` dir and set `RunLongTests: TRUE` in `.BBSoptions`; they run weekly (Saturdays, up to 6 hours) and their failures do not block propagation. Keep normal `tests/` under 40 minutes. +- GPU packages: declare `GPU_reliance: required` or `optional` in `.BBSoptions`. + +## Appendix C - Querying Web Resources + +- Keep downloads reasonably sized so `R CMD check` finishes well under 10 minutes. +- Never use unbounded `while()` retries; set an explicit max number of attempts (e.g. an `N.TRIES` loop wrapped in `tryCatch()`) and fail with a clear message that includes the URL and error. +- Respect `getOption("timeout")` and check HTTP status from `httr::GET()` / `download.file()`. + +## Appendix D - C and Fortran + +- Follow the "System and foreign language interfaces" section of Writing R Extensions. +- Use R's internal facilities (`R_alloc`, R's RNG) instead of system equivalents; register native routines. +- Add `R_CheckUserInterrupt()` in long C-level loops. +- Use `Makevars`/`Makefile` sparingly. During development enable all warnings and disable optimization, e.g. gcc `-Wall -Wextra -pedantic -O0 -ggdb`, clang `-Weverything -O0 -g` (put user Makevars in `~/.R/`). + +## Appendix E - C++/Mavericks Best Practices + +- Prefer Rcpp for C++ integration; use BH for Boost instead of bundling it. +- Define `R_NO_REMAP` and use fully-qualified names (`Rf_length()`, `std::map`); never `using namespace std;` (especially in headers). +- Keep R headers out of `extern "C"` blocks. +- Avoid dereferencing/incrementing past-the-end iterators (segfaults). Regenerate old SWIG code with a C++11-capable SWIG. + +## Appendix F - Man Page Links + +- `\linkS4class{}` cross-references (e.g. to `SummarizedExperiment`) can trigger check warnings. To resolve: put the target package in `Depends:` (not just `Imports:`), add `#' @import ` in roxygen, and run `devtools::document()` so NAMESPACE gets the `import()` entry. + +## Appendix G - Book News + +- Changelog for the guidelines. 1.0.0 (2021-06-02) initial release; 1.0.1 (2021-08-19) added the package-naming section. Tracks when new guidance was added. + +## Appendix H - References + +- Bibliography. Key citation: Soneson et al. (2025), "Eleven Quick Tips for Writing a Bioconductor Package," PLoS Computational Biology 21(3):e1012856. + +Source: https://contributions.bioconductor.org/use-devel.html , https://contributions.bioconductor.org/advanced-build-options.html , https://contributions.bioconductor.org/querying-web-resources.html , https://contributions.bioconductor.org/c-fortran.html , https://contributions.bioconductor.org/cmavericks-best-practices.html , https://contributions.bioconductor.org/man-links.html , https://contributions.bioconductor.org/booknews.html , https://contributions.bioconductor.org/references-1.html +Fetched 2026-07-23 from contributions.bioconductor.org (Bioconductor devel guide). diff --git a/knowledge/development/ai-policy.md b/knowledge/development/ai-policy.md new file mode 100644 index 0000000..b135855 --- /dev/null +++ b/knowledge/development/ai-policy.md @@ -0,0 +1,29 @@ +Covers: Chapter 19 - AI and third-party code policy + +# AI-Generated and Third-Party Code Policy + +## Disclosure +- Non-trivial code from AI tools or external sources must be disclosed: + discuss it in the submission issue and state it in the PR description. +- Cite provenance directly in the contributed code (in-code comment). +- Use an attribution line, for example: + - `Assisted-by: [AI name]` + - `Code copied from: [source/URL]` + +## Licensing +- All newly contributed code must be redistributable under the package's + existing license. +- The developer is responsible for confirming license compatibility before + incorporating any external or AI-generated code. + +## Maintainer responsibility +- The maintainer retains full accountability for all incorporated code, + regardless of origin, including its reliability, ongoing maintenance, and + bug fixes. + +## Scope +- The policy targets "non-trivial portions." Minor snippets or standard + boilerplate may fall outside it, but when in doubt, disclose. + +Source: https://contributions.bioconductor.org/ai-policy-third-party.html +Fetched 2026-07-23 from contributions.bioconductor.org (Bioconductor devel guide). diff --git a/knowledge/development/build-check-bioccheck.md b/knowledge/development/build-check-bioccheck.md new file mode 100644 index 0000000..d0bf3fd --- /dev/null +++ b/knowledge/development/build-check-bioccheck.md @@ -0,0 +1,57 @@ +Covers: Chapters 22 and 23 - Build, check, BiocCheck, and conclusion + +# Build, Check, and BiocCheck + +Run all three tools successfully before submission. Build and check against the +current Bioconductor devel version, using the matching R version (Bioconductor +releases are tied to specific R versions). + +## Exact commands +Build the source tarball first, then check the tarball it produces: + +``` +R CMD build MyPackage +R CMD check MyPackage_0.99.0.tar.gz +``` + +Then run BiocCheck. For a git clone / working directory: + +```r +BiocCheck::BiocCheckGitClone() +``` + +For a new-package submission (runs the stricter new-package rules): + +```r +BiocCheck::BiocCheck('new-package' = TRUE) +``` + +BiocCheck can also be run on the built tarball. During development, +`devtools::check()` is a convenient wrapper around `R CMD build`/`check`. + +## Pass/fail gate +- BiocCheck is the submission gate. The package MUST pass with no ERRORs and no + WARNINGs. Address NOTEs where possible; unresolved notes may be questioned in + review. +- `R CMD build` and `R CMD check` must complete without ERROR. +- New packages should carry version `0.99.x` in DESCRIPTION. + +## What BiocCheck enforces (selection) +- Bioconductor coding style, dependency and NAMESPACE correctness. +- Presence of a vignette, runnable examples, and unit tests. +- Correct DESCRIPTION fields including valid `biocViews`. +- No forbidden files tracked in git (see gitignore chapter). +- Function length, line length, and other style thresholds. + +## CI +- GitHub Actions workflows can mimic the submission environment; see the + R-Universe Bioconductor integration docs. + +## Submission (conclusion) +- All contributions undergo formal peer review. +- Submit through the official GitHub package submission repository/tracker. +- See "How to Build a Bioconductor Package with RStudio" for a walkthrough. +- Once build, check, and BiocCheck are clean, the package is ready to submit. + +Source: https://contributions.bioconductor.org/build-check-bioccheck.html and https://contributions.bioconductor.org/conclusion.html +Fetched 2026-07-23 from contributions.bioconductor.org (Bioconductor devel guide). diff --git a/knowledge/development/compiled-thirdparty.md b/knowledge/development/compiled-thirdparty.md new file mode 100644 index 0000000..da24bb3 --- /dev/null +++ b/knowledge/development/compiled-thirdparty.md @@ -0,0 +1,32 @@ +Covers: Chapter 17 - Code other than R (compiled and third-party) + +# Compiled Code and Third-Party Code + +## General rules +- Compiled code must follow the "System and foreign language interfaces" + section of the Writing R Extensions (R-exts) manual. +- Use `Makevars`/`Makefile` sparingly; they are often unnecessary. See the + "Configure and cleanup" section of R-exts. +- Code must be portable and build across all supported platforms + (Linux, macOS, Windows). Test on all before submission. +- Place compiled sources in `src/`. + +## Language-specific guidance +- C++: use Rcpp for cross-platform C++ integration (see the Rcpp Gallery). +- Fortran: consider dotCall64 for modern Fortran integration. +- Python: use basilisk to configure Python environments automatically so users + need no manual install. reticulate is at developer discretion. +- CMake-based builds: use the biocmake package. + +## Third-party code responsibilities +- Do not bundle external libraries that duplicate functionality already + provided by supported R/Bioconductor packages. +- Maintainers take full responsibility for any bundled third-party code: + keep it updated with upstream bug fixes and releases. +- Complex external libraries may require you to provide pre-built binaries for + some platforms. +- Ensure the license of any bundled third-party code is compatible with, and + redistributable under, the package license. + +Source: https://contributions.bioconductor.org/other-than-Rcode.html +Fetched 2026-07-23 from contributions.bioconductor.org (Bioconductor devel guide). diff --git a/knowledge/development/data.md b/knowledge/development/data.md new file mode 100644 index 0000000..2f7c460 --- /dev/null +++ b/knowledge/development/data.md @@ -0,0 +1,50 @@ +Covers: Chapter 14 - Including data in a package + +# Package Data + +## Size limits and where large data goes +- Keep the whole package small. Source tarballs should stay under roughly + 5 MB for software packages; do not bloat a software package with data. +- Large data sets must NOT be committed to the package. There is no Git-LFS + support. Distribute large data through ExperimentHub or AnnotationHub. +- Traditional (self-contained) experiment data packages need pre-approval on + the bioc-devel mailing list. Prefer Hub-based distribution instead. + +## Directory conventions +| Location | Purpose | Access | +|-----------------|----------------------------------|--------------------| +| `data/` | Exported R datasets | `data("mydata")` | +| `inst/extdata/` | Raw files parsed by workflows | `system.file()` | +| `R/sysdata.rda` | Internal, non-exported data | package-internal | + +- Small data used by examples, vignettes, and tests may ship directly in the + package (`data/` or `inst/extdata/`). +- Store scripts that generated the data in `inst/scripts/` (or `data-raw/`). + +## Formats and compression +- Preferred format for `data/`: `.RData` created with `save()`. Other formats + allowed (see `?data`). +- Compress all data files. +- Avoid `LazyData: true`. Despite general R advice, Bioconductor recommends + against it because it slows package loading when data is large. + +## Documentation requirements +- Every dataset must be documented: creation method, source, and intended use. +- Raw files in `inst/extdata/` need metadata describing derivation and format. + +## Caching and file-writing restrictions +- Forbidden: downloading or writing files to the user home directory, working + directory, or the installed package directory. +- For persistent caching use BiocFileCache (preferred) or + `tools::R_user_dir(package, which = "cache")`. +- For non-persistent scratch files use `tempdir()` / `tempfile()`. + +## ExperimentHub / AnnotationHub +- ExperimentHub: curated experiment data stored externally, retrieved on demand. +- AnnotationHub: annotation resources (genomic identifiers, mappings) served + externally. +- Both give a lightweight package: metadata + man pages + vignette, with the + heavy data hosted remotely. See the HubPub `CreateAHubPackage` vignette. + +Source: https://contributions.bioconductor.org/data.html +Fetched 2026-07-23 from contributions.bioconductor.org (Bioconductor devel guide). diff --git a/knowledge/development/documentation.md b/knowledge/development/documentation.md new file mode 100644 index 0000000..6d6d6d7 --- /dev/null +++ b/knowledge/development/documentation.md @@ -0,0 +1,40 @@ +# Documentation + +Covers: Chapter 13 - Documentation (man pages and vignettes). + +## Man pages (Rd files) +- Every exported function and class must have a man page. +- Class documentation must be very detailed about the structure and type of + information stored in the object. +- Data man pages must include provenance information and data structure + information. +- A package-level man page is encouraged, accessible via `?`. +- Documentation may be authored with roxygen2 (comments compiled to Rd) or written + directly as Rd. + +## Examples (runnable) +- All man pages should have runnable examples. +- `\donttest` and `\dontrun` are generally not allowed except with proper + justification. +- If wrapping is unavoidable, prefer `\donttest` over `\dontrun`. + +## Vignettes +- At least one vignette is required, in Rmd (recommended), qmd (Quarto), or Rnw + (Sweave) format. +- Code must be executable and demonstrate actual functionality: "Non-trivial + executable code is a must!!! Static vignettes are not acceptable." +- Each vignette should include an Introduction, Installation instructions, a Table + of Contents (when appropriate), and a `sessionInfo()` call. +- Installation instruction chunks must use `eval = FALSE`. +- R Markdown vignettes should use the BiocStyle package for rendering. +- Quarto vignettes require the Quarto command-line tool to be listed in + DESCRIPTION. + +## Overall documentation standards +- Vignettes demonstrating core functionality. +- Man pages for all exported functions, each with runnable examples. +- Well-documented data structures and datasets. +- References to the methods used and to related packages. + +Source: https://contributions.bioconductor.org/docs.html +Fetched 2026-07-23 from contributions.bioconductor.org (Bioconductor devel guide). diff --git a/knowledge/development/general-dev.md b/knowledge/development/general-dev.md new file mode 100644 index 0000000..2a33e20 --- /dev/null +++ b/knowledge/development/general-dev.md @@ -0,0 +1,66 @@ +# General Development Requirements + +Covers: Chapter 3 - General guidelines, and Chapter 4 - Important Bioconductor +package development features. + +## Development environment +- Develop against the devel version of Bioconductor and use devel Bioconductor + packages. +- Use a recent R-devel matching the current Bioconductor devel branch. + +## Pre-submission gate (build and check) +- Must pass `R CMD build` and `R CMD check` with NO errors and NO warnings on + recent R-devel. +- Must pass `BiocCheck::BiocCheckGitClone()` with no errors/warnings. +- Must pass `BiocCheck::BiocCheck('new-package' = TRUE)` with no errors/warnings. +- All ERRORs, WARNINGs, and NOTEs must be addressed or explicitly justified. + +## Numeric thresholds (note the modality - only one of these is a "must") +- Individual files (software packages): **<= 5 MB** each. Upstream: "individual files must be + <= 5MB". +- Source package produced by `R CMD build`: **< 10 MB**. Upstream: "should occupy less than + 10 MB on disk". +- `R CMD check --no-build-vignettes` runtime: **< 10 minutes**. Upstream: "should require less + than 10 minutes to run R CMD check --no-build-vignettes". +- Memory across vignettes, examples, and tests: **< 8 GB**. Upstream: "it is recommended that + the vignettes, man page examples, and unit tests do not require more than 8 GB of memory". +- Use lossy compression (e.g., pngquant) to shrink large images/screenshots. + +Treat the three "should" items as strong expectations: the build system enforces them in practice +and a reviewer will ask. But do not tell a submitter they are blocked from submitting by them. + +## File hygiene +- Do not include filenames that differ only in case (cross-platform safety). +- Exclude unnecessary files: `.DS_Store`, `.project`, `.git`, cache files, logs, + `*.Rproj`, `*.so`. +- Use `.gitignore` to keep undesirable files out of the repository. +- Application-specific tooling (GitHub Actions, devtools config) "should be in a different + branch" than the default one holding package code. A recommendation upstream, not a rule. +- R CMD check options are customized by Bioconductor via flags configurable + through the `R_CHECK_ENVIRON` environment variable. + +## biocViews (required feature) +- The DESCRIPTION file MUST contain a `biocViews:` field (case-sensitive, + lowercase 'b'). +- Choose terms from only ONE category: Software, Annotation Data, Experiment Data, + or Workflow. +- Use leaf-level terms rather than broad parent categories. +- Terms must match the official hierarchy exactly (spelling and capitalization); + consult the devel branch biocViews list. +- Submission validation checks that biocViews are present, valid, and from a + single category. + +## Vignettes (required feature) +- Every submitted package must have at least one Rmd (preferred) or Rnw vignette. +- Render with `BiocStyle::html_document`. +- Vignettes must contain evaluated (non-trivial, runnable) R code. +- Include a detailed introduction motivating inclusion in Bioconductor and, where + relevant, compare against existing similar packages. + +## Reuse existing infrastructure +- Reuse established Bioconductor classes and methods where appropriate (see the + Common Bioconductor Methods and Classes guidance / methods-classes.md). + +Source: https://contributions.bioconductor.org/general.html and +https://contributions.bioconductor.org/important-bioconductor-package-development-features.html +Fetched 2026-08-14 from contributions.bioconductor.org (Bioconductor devel guide). diff --git a/knowledge/development/gitignore.md b/knowledge/development/gitignore.md new file mode 100644 index 0000000..14f01c1 --- /dev/null +++ b/knowledge/development/gitignore.md @@ -0,0 +1,34 @@ +Covers: Chapter 21 - .gitignore + +# .gitignore + +- Keep a single `.gitignore` file at the top level of the package. Do not nest + multiple `.gitignore` files. +- Certain system/generated files must NOT be tracked. BiocCheck flags them if + committed. They may exist locally but must be excluded. + +## Files that must be excluded (BiocCheck-flagged) +Configuration and history: +- `.Renviron`, `.Rprofile`, `.Rhistory`, `.RApp.history` +- `.Rproj`, `.Rproj.user` +- `.seed`, `.exrc`, `.gdb.history` + +Build artifacts and compiled objects: +- `.o`, `.sl`, `.so`, `.dylib`, `.a`, `.dll`, `.def` +- `.log`, `.aux`, `.backups` + +System and IDE files: +- `.DS_Store` (macOS) +- `.project`, `.cproject`, `.settings`, `.tm_properties` +- `.directory`, `.dropbox` +- `unsrturl.bst` + +Git metadata that should not be present: +- `.gitattributes`, `.gitmodules`, `.hgtags` + +## Notes +- Do not commit build tarballs (`*.tar.gz`) or the `*.Rcheck/` directory. +- Do not commit large data files (see data chapter); they belong in a Hub. + +Source: https://contributions.bioconductor.org/gitignore.html +Fetched 2026-07-23 from contributions.bioconductor.org (Bioconductor devel guide). diff --git a/knowledge/development/metadata-files.md b/knowledge/development/metadata-files.md new file mode 100644 index 0000000..f949c6b --- /dev/null +++ b/knowledge/development/metadata-files.md @@ -0,0 +1,121 @@ +# Metadata Files + +Covers: Chapters 6-12 - README, DESCRIPTION, NAMESPACE, NEWS, LICENSE, CITATION, +and system dependencies. + +## README (Chapter 6) +- README is optional but useful, especially for packages developed on GitHub. +- If present, it must clearly give Bioconductor installation instructions using + `BiocManager::install()`. +- Any installation code blocks must use `eval = FALSE` so they do not execute. +- Files with executable code (including `README.Rmd`) must NOT install packages, + download system dependencies, or download applications; assume dependencies are + already present. +- Declare external software in DESCRIPTION `SystemRequirements`, not in the README. +- `README.md` may be auto-generated from a vignette via `README.Rmd` (R Markdown + child documents) and `rmarkdown::render()`. + +## DESCRIPTION (Chapter 7) +Required fields: +- **Package** - must match the repository name (case-sensitive). +- **Title** - brief but descriptive summary. +- **Version** - `x.y.z` scheme. New submissions start at **0.99.0**. `y` is even + for release versions, odd for devel; `z` increments with each commit. +- **Description** - relatively short but detailed overview; at least three complete + sentences. +- **Authors@R** - required (use this, not `Authors:`). Must include the maintainer + with the `cre` role and an actively maintained email. Use a single maintainer. + Include ORCID in `comment` if available. Example: + `person("First", "Last", email = "me@x.org", role = c("cre", "aut"), comment = c(ORCID = "..."))`. +- **License** - standard R license spec, version-specific (see LICENSE section). +- **biocViews** - REQUIRED (case-sensitive, lowercase 'b'). At least two leaf + nodes, all from the same trunk/package type; single comma-separated line. + +Dependency fields (Depends, Imports, Suggests, Enhances): +- All dependencies must come from Bioconductor or CRAN. The `Remotes:` field is + NOT supported. +- List each package only once across these fields. +- **Imports** - functions/methods/classes used within the package namespace + (the usual place for dependencies). +- **Depends** - only for functionality essential to users; rarely more than 3 + packages (avoid Depends bloat). +- **Suggests** - packages used only in vignettes, examples, or conditional code. +- **Enhances** - optional performance packages such as `Rmpi` or `parallel`. +- Version specifications are usually not needed. + +Other fields: +- **LazyData** - omit `LazyData: TRUE` for large data packages (it slows loading). +- **SystemRequirements** - external software not auto-installed; add an INSTALL + file for non-trivial installs. +- **BugReports** - encouraged; link to the GitHub issues page. +- **URL** - source repo and help resources. +- **VignetteBuilder** - name the builder (e.g., `knitr`) when using vignettes. +- **BiocType** - required for Docker/Workflow submissions; values `Software`, + `ExperimentData`, `Annotation`. +- **Config/Bioconductor/UnsupportedPlatforms** - comma-separated list to exclude + platforms (`windows`, `windows-x64`, `macosx`, `macosx-x86_64`, `macosx-arm64`). + +## NAMESPACE (Chapter 8) +- Prefer `importFrom()` to import specific functions; use `import()` only when + importing many functions from one package. +- For Bioconductor classes, `import()` the whole package so full class + functionality is inherited automatically. +- Do NOT use broad export patterns: `exportPattern("^[[:alpha:]]+")` is strongly + discouraged and almost always not allowed. Export functions/generics + individually. +- Exported function names should use camelCase or underscores; avoid dots (dots + imply S3 dispatch). Functions beginning with `.` stay internal and are not + exported. +- Use `exportMethods()` / `exportClasses()` for S4 methods and classes, and + `useDynLib()` for compiled code. + +## NEWS (Chapter 9) +- Exactly one NEWS file per package, in one of: `./inst/NEWS.Rd`, `./inst/NEWS`, + `./inst/NEWS.md`, `./NEWS.md`, or `./NEWS`. +- NEWS files MUST use list elements/structure; plain text files are not allowed. +- Document the forthcoming release version, in non-technical language. +- Version heading format: + ``` + CHANGES IN VERSION X.Y.Z + ------------------------- + ``` +- Use section headers such as "NEW FEATURES" and "SIGNIFICANT USER-VISIBLE + CHANGES" with bullet points. +- Bioconductor compiles NEWS files into semi-annual release announcements. +- Validate formatting with `utils::news(package = "")`. + +## LICENSE (Chapter 10) +- Use R's standard license specifications (r-project.org/Licenses). Be version + specific (e.g., `GPL-2`). Core packages typically use `Artistic-2.0`. +- Forbidden: licenses restricting use (e.g., to academic or non-profit + researchers) and other restrictive licenses (CC BY-NC variants, ACM). +- For a non-standard license, add a full `LICENSE` file at the package root and + reference it as `file LICENSE`; the file must match the `License:` field. +- All dependencies must have compatible open-source licenses, and the package must + contain only code that can be redistributed under its license. + +## CITATION (Chapter 11) +- Place the file at `inst/CITATION`. It is optional but recommended. +- Validate with `readCitationFile("inst/CITATION")` (must run without errors) so it + displays correctly on the package landing page. +- Follow Writing R Extensions conventions; specify author/maintainer details for + correct name formatting. If absent, Bioconductor auto-generates a citation. +- Also include citations in help pages and vignettes. + +## System dependencies (Chapter 12) +- Declare external software in the DESCRIPTION `SystemRequirements` field, with an + optional INSTALL file giving install instructions for Linux, Windows, and Mac. +- System requirements must never be exclusive to a specific version; work with + current versions of the external software. +- Declaring a requirement does not guarantee Bioconductor will agree to install it. +- Discuss additional system requirements on bioc-devel@r-project.org before + development. Do not install system dependencies from within package code. + +Source: https://contributions.bioconductor.org/readme.html, +https://contributions.bioconductor.org/description.html, +https://contributions.bioconductor.org/namespace.html, +https://contributions.bioconductor.org/news.html, +https://contributions.bioconductor.org/license.html, +https://contributions.bioconductor.org/citation.html, +https://contributions.bioconductor.org/sysdep.html +Fetched 2026-07-23 from contributions.bioconductor.org (Bioconductor devel guide). diff --git a/knowledge/development/methods-classes.md b/knowledge/development/methods-classes.md new file mode 100644 index 0000000..a9f488d --- /dev/null +++ b/knowledge/development/methods-classes.md @@ -0,0 +1,42 @@ +# Reusing Common Methods and Classes + +Covers: Chapter 5 - Common Bioconductor methods and classes. + +## Core principles +- Interoperability is required: packages are generally NOT accepted unless they + demonstrate interoperability, typically by reusing existing Bioconductor classes + and methods where appropriate. +- Bioconductor uses the S4 object system for genomic data because it provides + formal class definitions, multiple inheritance, and validity checking. +- New classes require strong justification and must clearly describe how they + interoperate with existing Bioconductor infrastructure. +- Before creating new classes, discuss the proposal on the bioc-devel mailing list + or Bioconductor Slack for community feedback. + +## Classes to reuse (by data type) +| Data type | Recommended class / package | +|-----------|-----------------------------| +| Count matrices, microarray data | `SummarizedExperiment::SummarizedExperiment()` | +| Genomic coordinates | `GenomicRanges::GRanges()` | +| Multi-sample genomic coordinates | `GenomicRanges::GRangesList()` | +| Variable-length / ragged coordinates | `RaggedExperiment::RaggedExperiment()` | +| DNA/RNA/protein sequences | `Biostrings::*StringSet()` | +| Gene sets / collections | `BiocSet::BiocSet()` or `GSEABase` equivalents | +| Multi-omics integration | `MultiAssayExperiment::MultiAssayExperiment()` | +| Single-cell data | `SingleCellExperiment::SingleCellExperiment()` | +| Mass spectrometry | `Spectra::Spectra()` | + +## Import / parsing methods to reuse +Use existing importers instead of writing custom parsers: +- Genomic file formats (BED, GFF, etc.): `rtracklayer` +- VCF: `VariantAnnotation` +- BAM / sequencing alignments: `Rsamtools`, `GenomicAlignments` +- FASTA sequences: `Biostrings` +- Mass spectrometry data: `Spectra` + +## When importing Bioconductor classes +- Import the full class package (via `import()`) so that full class functionality + is inherited automatically (see namespace guidance in metadata-files.md). + +Source: https://contributions.bioconductor.org/reusebioc.html +Fetched 2026-07-23 from contributions.bioconductor.org (Bioconductor devel guide). diff --git a/knowledge/development/non-software-pkgs.md b/knowledge/development/non-software-pkgs.md new file mode 100644 index 0000000..d8683b0 --- /dev/null +++ b/knowledge/development/non-software-pkgs.md @@ -0,0 +1,41 @@ +Covers: Chapter 20 - Non-software packages + +# Non-Software Packages + +Non-software packages fall into two families: annotation packages and +experiment data packages. Hub-based distribution is preferred over +self-contained data packages. + +## Annotation packages +- Link identifiers (gene names, probe IDs) to related information + (chromosomal location, Gene Ontology categories, mappings). +- Must include proper documentation for the data provided. + +## Experiment data packages +- Contain curated datasets from an experiment, course, or publication, + typically a single dataset. +- Require documentation of the data (source, creation, use). +- Traditional self-contained experiment data packages are discouraged; prefer + the Hub approach. + +## Hub packages (AnnotationHub / ExperimentHub) +- Lightweight: data is stored externally (AWS S3, Azure Data Lakes, Ensembl, + other public sites) and fetched on demand. +- Must minimally contain: resource metadata, man pages describing the + resources, and a vignette. May include supporting R functions. +- Follow the `CreateAHubPackage` vignette in the HubPub package. + +## biocViews +- Annotation packages must include `AnnotationData` (and appropriate child + terms) in the DESCRIPTION `biocViews:` field. +- Experiment data packages must include `ExperimentData` (and child terms). +- Correct biocViews determine which of the three Bioconductor repositories the + package is assigned to. + +## Submission +- Submit through the GitHub package submission tracker. +- Indicate the package type (annotation, experiment data, workflow) at + submission. + +Source: https://contributions.bioconductor.org/non-software.html +Fetched 2026-07-23 from contributions.bioconductor.org (Bioconductor devel guide). diff --git a/knowledge/development/package-name.md b/knowledge/development/package-name.md new file mode 100644 index 0000000..aa014b4 --- /dev/null +++ b/knowledge/development/package-name.md @@ -0,0 +1,37 @@ +# Package Name + +Covers: Chapter 2 - Choosing a package name. + +## Core rules +- The package name must match the GitHub repository name and is case-sensitive. +- The name must be unique: it must not already exist in Bioconductor (checked + case-insensitively) or on CRAN. +- The name should be descriptive of the package's purpose. +- Reusing archived or deprecated package names is strongly discouraged and often + will not be allowed. + +## How to check availability +- Try installing the proposed name; the install should FAIL if the name is free: + ```r + BiocManager::install("MyPackage") + ``` +- Alternatively, search the Bioconductor code base / package listings directly. +- Check for unintended meanings in other languages using a tool such as + wordsafety.com. + +## Names to avoid (forbidden / disallowed patterns) +- Names that create confusion with an existing package, function, or class name. +- Names implying temporal relationships (e.g., `ExistingPackage2`) or qualitative + upgrades (e.g., `ExistingPackagePlus`). +- Hate speech, slurs, or profanity. +- References to historical, ethical, or political contexts. +- Names invoking well-known people, characters, brands, places, or icons. +- Names with unintended meanings in foreign languages. + +## Renaming +- Bioconductor discourages renaming a package after acceptance. +- Renaming requires deprecating the old package and resubmitting for review, which + is time-consuming. Choose carefully up front. + +Source: https://contributions.bioconductor.org/package-name.html +Fetched 2026-07-23 from contributions.bioconductor.org (Bioconductor devel guide). diff --git a/knowledge/development/r-code.md b/knowledge/development/r-code.md new file mode 100644 index 0000000..3e0ef27 --- /dev/null +++ b/knowledge/development/r-code.md @@ -0,0 +1,62 @@ +Covers: Chapter 16 - R code + +# R Code Style and Best Practices + +## Formatting (hard rules) +- Use `<-` for assignment; `=` only for named function arguments. +- Indent with 4 spaces. No tabs. +- No lines longer than 80 characters. +- Space around binary operators: `a == b`. No space around `=` in named + arguments: `somefunc(a=1, b=2)`. +- Full-line comments start with `##`, indented to surrounding code. + +## Naming +- Functions and variables: camelCase starting lowercase (`myFunction`). +- Classes: CamelCase starting uppercase (`MyClass`). +- Do NOT put `.` in function names (avoids S3 dispatch collisions). +- Prefix non-exported/internal functions with a dot: `.internalFunc`. + +## Vectorization and iteration +- Write `seq_len(n)` or `seq_along(x)`, NOT `1:n` or `1:length(x)` + (the latter break when length is 0). +- Prefer vectorized code over explicit `for` loops. +- Use `vapply()` instead of `sapply()` (type-safe). +- Pre-allocate and fill (via `lapply()`/`vapply()`); never copy-and-append in a + loop (that is O(n^2)). + +## Booleans +- Use `TRUE`/`FALSE`, never `T`/`F`. + +## Functions +- Write small functions; avoid functions longer than one screen. +- Give arguments defaults where sensible; validate with `stopifnot()` or checks. + +## Forbidden patterns +- `set.seed()` inside package/internal code. +- `browser()` left in code. +- Direct slot access with `@` or `slot()` - use accessor methods instead. +- `<<-` (superassignment). +- `system()` without justification - use `system2()`. +- Nested function definitions. +- Commented-out code blocks and TODO comments in published packages. + +## Messaging +- `message()` for diagnostic messages. +- `warning()` for unusual-but-handled situations. +- `stop()` for errors. +- `cat()`/`print()` only inside `show()` methods, not for general messaging. + +## Classes and methods +- Prefer S4 over S3. Provide constructor functions and `show()` methods. +- Use accessors, not direct slot access. +- Only define methods for classes exported by your own package. +- Reuse existing Bioconductor core classes rather than inventing new ones. + +## Web access, caching, parallelism +- Never write to the user home, working, or installed-package directory. +- Cache downloads via BiocFileCache or + `tools::R_user_dir(package, which="cache")`; use `tempfile()` for scratch. +- Parallel operations should default to 1 or 2 cores. + +Source: https://contributions.bioconductor.org/r-code.html +Fetched 2026-07-23 from contributions.bioconductor.org (Bioconductor devel guide). diff --git a/knowledge/development/shiny.md b/knowledge/development/shiny.md new file mode 100644 index 0000000..244c29b --- /dev/null +++ b/knowledge/development/shiny.md @@ -0,0 +1,38 @@ +Covers: Chapter 18 - Shiny apps in packages + +# Shiny Apps + +## Code location and organization +- UI and server code for a submitted Shiny app must live under the package + `R/` directory (not in a top-level `app.R` / `inst/shiny`). +- Keep business logic out of `shinyApp()` calls. Build internal functions that + generate UI and server components separately, so logic is testable without + launching the app. +- Recommended file naming: + - `interface_*.R` - functions returning UI elements + - `outputs_*.R` - functions returning server outputs + - `observers_*.R` - functions creating reactive observers + - `utils_*.R` - misc processing helpers + +## Launching restrictions +- `shiny::runApp()` must NOT appear anywhere in the package source. +- Exported functions should RETURN a Shiny app object; the user calls + `runApp()` themselves. + +## Testing +- Unit-test all non-reactive functions (e.g. with testthat). +- Wrap untestable reactive code with `# nocov start` / `# nocov end`. +- Use shinytest2 to test visual and computational aspects of the app. +- Put reusable test fixtures in `tests/testthat/setup-*.R`. + +## Documentation +- Wrap example code that launches an app in `if (interactive()) { ... }`. +- Document internal functions with roxygen2 `@keywords internal`. +- Include screenshots in the vignette; optimize with pngquant or webshot2. + +## Review expectations +- Reviewers run `R CMD build`, `R CMD check`, and BiocCheck, test the app with + shinytest2, and check graceful error handling and responsive UI. + +Source: https://contributions.bioconductor.org/shiny.html +Fetched 2026-07-23 from contributions.bioconductor.org (Bioconductor devel guide). diff --git a/knowledge/development/tests.md b/knowledge/development/tests.md new file mode 100644 index 0000000..cbfd777 --- /dev/null +++ b/knowledge/development/tests.md @@ -0,0 +1,42 @@ +Covers: Chapter 15 - Unit tests + +# Unit Testing + +## Framework choice +- Bioconductor slightly prefers testthat. RUnit and tinytest are also accepted. +- testthat: active development, rich assertions, integrates with devtools, + informative failures. +- tinytest: lightweight, zero dependencies. +- RUnit: long Bioconductor history but unmaintained since ~2010. +- Declare the framework in DESCRIPTION `Suggests:` (e.g. `Suggests: testthat`, + or `Suggests: RUnit, BiocGenerics`, or `Suggests: tinytest`). + +## Directory structure and naming +- testthat: tests in `tests/testthat/`, files start with `test`. + Set up with `usethis::use_testthat()`. +- RUnit: tests in `inst/unitTests/`, files match `test_*.R` + (e.g. `test_divideBy.R`). Add `tests/runTests.R` containing: + `BiocGenerics:::testPackage("MyPackage")`. +- tinytest: tests in `inst/tinytest/`. Add `tests/tinytest.R`: + `if (requireNamespace("tinytest", quietly=TRUE)) tinytest::test_package("PACKAGE")`. + +## What to test +- Test functions, methods, and classes with known inputs and expected outputs. +- Test edge cases and error conditions, not only the happy path. +- No hard minimum coverage percentage is mandated, but higher coverage is + expected and reduces bug risk. + +## Coverage measurement +- Use the covr package: `covr::package_coverage()`. + +## Running tests +- Full check (runs all tests): `R CMD check MyPackage`. +- During development: `devtools::test()` (reloads code and reruns). +- Manual: source the package and test files, then call the test function. + +## Long-running tests +- Consult the bioc-devel mailing list before adding tests that run very long, + so they do not slow the nightly builds. + +Source: https://contributions.bioconductor.org/tests.html +Fetched 2026-07-23 from contributions.bioconductor.org (Bioconductor devel guide). diff --git a/knowledge/index.md b/knowledge/index.md new file mode 100644 index 0000000..8e24104 --- /dev/null +++ b/knowledge/index.md @@ -0,0 +1,63 @@ +# Bioconductor knowledge base - router + +Task-oriented summaries of the official guide "Bioconductor Packages: Development, +Maintenance, and Peer Review" (https://contributions.bioconductor.org). Open the file that +matches the task. Each summary links back to its canonical chapter for the full text. + +## Start here +- New to submission, or want the whole path end to end: read `workflow.md` (the runbook). +- Just need one topic: use the map below. + +## Lifecycle router + +Authoring a package: +- Naming: `development/package-name.md` (ch 2) +- General setup + key features: `development/general-dev.md` (ch 3-4) +- Reusing Bioc classes/methods (SummarizedExperiment, S4, etc.): `development/methods-classes.md` (ch 5) +- Metadata files (README, DESCRIPTION, NAMESPACE, NEWS, LICENSE, CITATION, INSTALL): + `development/metadata-files.md` (ch 6-12) +- Documentation (vignettes, man pages, roxygen): `development/documentation.md` (ch 13) +- Package data + large data (ExperimentHub/AnnotationHub): `development/data.md` (ch 14) +- Unit tests: `development/tests.md` (ch 15) +- R code + Bioc code style: `development/r-code.md` (ch 16) +- Compiled / third-party code (C/C++/Fortran/Python): `development/compiled-thirdparty.md` (ch 17) +- Shiny apps: `development/shiny.md` (ch 18) +- AI policy + third-party code: `development/ai-policy.md` (ch 19) +- Non-software packages (ExperimentData/Annotation/Workflow): `development/non-software-pkgs.md` (ch 20) +- .gitignore: `development/gitignore.md` (ch 21) +- Build / Check / BiocCheck (the gate): `development/build-check-bioccheck.md` (ch 22-23) + +Submitting: +- Eligibility, package types, tracker issue, Single Package Builder: `01-submissions.md` (ch 1) +- The full sequence from local build to first release: `workflow.md` + +Maintaining (after acceptance): +- Git workflow (BiocCredentials, git.bioconductor.org dual remotes), version numbering, + build-report troubleshooting, deprecation, end of life, branch rename: `maintenance.md` (ch 24-30) + +Reviewing (and what reviewers check): +- Review expectations, reviewer tools, volunteering: `reviewer.md` (ch 31-33) + +Appendices: +- Using devel Bioconductor, advanced build options, querying web resources, C/Fortran, Mavericks, + Rd links, NEWS, references: `appendices.md` (A-H) + +## The gate (memorize) +See `workflow.md` and `development/build-check-bioccheck.md` for detail, including which items are +requirements and which are recommendations - the distinction matters when telling someone whether +they can submit. + +Requirements: +- Pass `R CMD check` clean on current R-devel (no errors, no warnings). +- Pass `BiocCheck::BiocCheckGitClone()` and `BiocCheck::BiocCheck('new-package' = TRUE)` clean. +- Individual files <= 5 MB. +- Include `biocViews`, a vignette, and man pages; valid maintainer email equal to the submitter; + not already on CRAN; hosted on the GitHub default branch. + +Recommendations (expected in practice, but upstream says should/recommended): +- Set `Version: 0.99.0`. +- Source build < 10 MB; `R CMD check --no-build-vignettes` < 10 min; < 8 GB memory to run + vignettes/examples/tests. + +Source: https://contributions.bioconductor.org/index.html +Fetched 2026-08-14 from contributions.bioconductor.org (Bioconductor devel guide). diff --git a/knowledge/maintenance.md b/knowledge/maintenance.md new file mode 100644 index 0000000..4e28d35 --- /dev/null +++ b/knowledge/maintenance.md @@ -0,0 +1,190 @@ +# Covers: Chapters 24-30 - Bioconductor Package Maintenance + +Overview: this knowledge base covers ongoing maintenance and development of packages +already accepted into Bioconductor - git workflow, version numbering, build-report +troubleshooting, C/C++ debugging, deprecation, end-of-life policy, and branch renaming. + +## Chapter 24 - Git Version Control (post-acceptance workflow) + +Setup after acceptance: +- Register your SSH public key at the BiocCredentials app: `https://git.bioconductor.org/BiocCredentials/`. + Bioconductor initially seeds from your GitHub keys at `https://github.com/.keys`. +- Add the Bioconductor server as the `upstream` remote; keep `origin` = your GitHub repo: + ```bash + git remote add upstream git@git.bioconductor.org:packages/.git + git remote -v # origin -> GitHub, upstream -> git.bioconductor.org + ``` + +Sync before editing (pull from BOTH remotes so they stay in step): +```bash +git fetch --all +git merge upstream/devel +git merge origin/devel +``` + +Commit and push to BOTH remotes (every commit must bump the `z` version - see ch25): +```bash +git add +git commit -m "informative message" +git push upstream devel +git push origin devel +``` + +Push rules: +- Only two branches accept maintainer pushes: `devel` and the current release branch + (e.g. `RELEASE_3_6`). New branches CANNOT be created/pushed to the Bioconductor server. +- Use SSH exclusively for developer (read/write) access. + +Backport a bug fix from devel to release with cherry-pick: +```bash +git checkout RELEASE_3_6 +git cherry-pick +git push upstream RELEASE_3_6 +``` + +Builds: run once per day and take roughly 24 hours. A valid version bump is required +for any change to propagate; broken packages are not published to users. + +## Chapter 25 - Version Numbering + +Format is `x.y.z`: +- New (unaccepted) packages start at `0.99.0` in DESCRIPTION. Upstream: "New packages submitted + to Bioconductor should set Version: 0.99.0 in the DESCRIPTION file." Stated as a "should", but + it is what reviewers and the build system expect - set it, and treat a wrong version as a + warning rather than as a reason someone cannot submit. +- `y` (middle): must be ODD in devel, EVEN in release. Maximum value is 99. +- `z` (patch): increment by 1 for EACH git commit in the devel branch. +- `x` (major): only ever changed by the Bioconductor team. + +Release transition mechanics: +- At release, a `0.99.z` package becomes `1.0.0` (first official release), and devel + continues from `1.1.0`. +- Generally, a package at `x.99.z` is bumped to `(x+1).0.0` in release and `(x+1).1.0` in devel. +- For a regular devel version like `1.1.25`, the team creates the release branch at `1.2.0` + and bumps devel to `1.3.0`. + +Critical: commits pushed WITHOUT a corresponding version bump do NOT propagate to the +repository seen by `BiocManager::install()`. + +## Chapter 26 - Troubleshooting the Build Report + +Propagation timeline: +- The Bioconductor Build System (BBS) pulls code daily around 2:30 PM EST; reports appear + around 11:30 AM EST next day. Commits after the cutoff slip to the following day + (36-48h lag possible). +- A valid version bump is ALWAYS required to propagate; broken packages are not published. + +Reproduce build failures locally: +1. Match the R version shown at the top of the relevant (devel/release) build report. +2. Update dependencies: `BiocManager::valid()` then `BiocManager::install()`. +3. Apply the build system's environment variables (Renviron.bioc). +4. Consider the official Bioconductor Docker images for a pre-configured environment. + +Common error categories: +- R 4.3+: vectors in `if`/`&&`/`||` conditions now error - reduce with `any()`/`all()`. +- R 4.0: missing S3 method registration in NAMESPACE; partial arg matching no longer + tolerated; `matrix` now extends `array` (use `is()`/`inherits()`, not `class(x) == ...`); + `data.frame()`/`read.table()` default `stringsAsFactors = FALSE`. +- Dependencies: CRAN binaries may lag a new R version; packages removed from CRAN/Bioc + force code restructuring; missing system libraries need a GitHub issue report. + +## Chapter 27 - Debugging C/C++ Code + +Setup: +- Compile without optimization and with debug symbols. Set `CFLAGS=-ggdb -O0` + (and `CXXFLAGS`) in `~/.R/Makevars`. +- Write a minimal script `buggy.R` that triggers the crash quickly and reliably. + +Tools: +- Valgrind (memory errors - invalid reads/writes, corruption behind segfaults): + ```bash + R -d valgrind -f buggy.R + ``` +- gdb / lldb (interactive): + ```bash + R -d gdb -f buggy.R + ``` + Key commands: `r` run, `b ` breakpoint, `bt` backtrace/call stack, + `p ` inspect a C variable, `call Rf_PrintValue()` to print an R object at C level. + +Workflow: locate the crash frame via `bt` (low-numbered frames are where execution +entered the bad code), inspect nearby state, test a hypothesized fix, rerun, confirm. +Debuggers reveal WHERE a crash happens; you deduce WHY. + +## Chapter 28 - Deprecation Guidelines + +Applies to functionality present in at least one official release; features added and +removed within the same devel cycle are exempt. Full lifecycle spans ~3 release cycles (~18 months). + +Function deprecation - three steps across cycles: +1. Deprecate (this devel cycle): inside the function call `.Deprecated("newFunc")` to emit + a warning; note the replacement in the man page. + ```r + myOldFunc <- function() { .Deprecated("myNewFunc") } + ``` +2. Defunct (next release cycle): replace `.Deprecated()` with `.Defunct()` so the function + errors instead of running; remove its man page and add it to a `MyPkg-defunct` man page. +3. Remove (following cycle): delete the code and its NAMESPACE export; keep only the defunct + man page so `help("MyPkg-defunct")` still works. + +Datasets: +- S3: add a deprecation class + custom `print` method that warns; next cycle make it error, then remove. +- S4: `setClass()` a deprecation subclass of the original + a `show` method that warns; same timeline. + +Whole packages: see the End-of-Life policy (ch29). + +## Chapter 29 - Package End-of-Life Policy + +The Core Team deprecates packages that: +- Fail to build/check cleanly on all platforms (maintainer gets a final ~2-week notice), or +- Have unresponsive maintainers (must answer support-site questions and package email, and + keep a valid maintainer email address). + +Timeline: +- Step I - Deprecation: ~6-month warning; users see deprecation notices and strikethrough on + build reports. +- Step II - Defunct: after one devel cycle without a fix, removed from nightly builds and + from `BiocManager::install()`. +- Final removal: package disappears in the following release cycle. + +Recovery: a package can return to active status if fixed within the deprecation period - +contact `maintainer@bioconductor.org`. A fully defunct/removed package must go through full +new-package review again. + +Maintainer-initiated: maintainers may voluntarily request deprecation (superseded, outdated, +unmaintainable) by notifying the bioc-devel mailing list. + +Orphaned packages: unresponsive-maintainer packages are labeled "orphaned"; community members +can request takeover by emailing the original maintainer and the Bioconductor team. + +## Chapter 30 - Branch Rename FAQs + +Bioconductor uses `devel` as the default branch name. Only the central repo at +`git.bioconductor.org` is renamed by the core team; maintainers must update their own local +and GitHub clones. Developers/maintainers are affected; end users are not. + +Rename a local branch to `devel`: +```bash +git branch -m master devel # or: git branch -m main devel +git fetch origin +git branch -u origin/devel devel +git remote set-head origin -a +``` + +On GitHub: change the default branch at +`https://github.com///branches` (edit pencil) to `devel`. + +Then push to Bioconductor: +```bash +git checkout devel +git push upstream devel +``` + +Clean up stale references: +```bash +git remote prune origin --dry-run # verify +git remote prune origin # execute +``` + +Source: https://contributions.bioconductor.org/git-version-control.html and https://contributions.bioconductor.org/versionnum.html and https://contributions.bioconductor.org/troubleshooting-build-report.html and https://contributions.bioconductor.org/debugging-cc-code.html and https://contributions.bioconductor.org/deprecation.html and https://contributions.bioconductor.org/package-end-of-life-policy.html and https://contributions.bioconductor.org/branch-rename-faqs.html +Fetched 2026-08-14 from contributions.bioconductor.org (Bioconductor devel guide). diff --git a/knowledge/reviewer.md b/knowledge/reviewer.md new file mode 100644 index 0000000..768eefe --- /dev/null +++ b/knowledge/reviewer.md @@ -0,0 +1,46 @@ +# Reviewer Resources + +Covers: what reviewers check, reviewer tools, and how to volunteer to review. Doubles as an author-facing "what the reviewer will look for" checklist. + +## Chapter - Reviewer Resources Overview + +- Reviews are public: every submission review happens in the open on the `Bioconductor/BiocContributions` GitHub issue tracker. +- Anyone in the community may comment on any review, not just the assigned reviewer. +- All feedback must follow the Bioconductor Code of Conduct. +- Three resources make up this section: review expectations (what/how to review), reviewer tools (checklist + examples), and volunteer sign-up. + +## Chapter 31 - Review Expectations + +Who reviews and the commitment: + +- Reviewers must maintain at least one active Bioconductor package. +- Budget roughly 30 minutes to 1.5 hours per package review. +- Complete the review within 3 weeks of assignment. + +What reviewers evaluate (author-facing checklist of focus areas): + +- Ease of use of the package (intuitive API, sensible defaults). +- Documentation quality: complete man pages, a runnable vignette, clear examples. +- Well-written code: readable, maintainable, follows Bioconductor coding style. +- Interoperability: reuse of core Bioconductor classes and infrastructure rather than reinventing them. + +Note: Chapter 31 sets the process and focus areas. The detailed, item-by-item checklist lives in the reviewer tools chapter (below) and in the package development guidelines chapters. + +## Chapter 32 - Reviewer Tools + +- Package Review Checklist: a ready-made template reviewers paste into the relevant New Submission Tracker issue and tick off / update as the review proceeds. +- New Submission Tracker: the `Bioconductor/BiocContributions` GitHub issues are where reviews are conducted and the checklist is posted and tracked over time. +- Example reviews: completed reviews (e.g. MAGAR, HubPub, BiocSet) are linked as references for the expected standard and tone. +- The checklist plus automated `BiocCheck` / `R CMD check` output from the Single Package Builder drive the concrete pass/fail items an author must resolve. + +Authors: expect the reviewer to walk this checklist publicly in your submission issue, so pre-run `BiocCheck` and clear ERRORs/WARNINGs before requesting review. + +## Chapter 33 - Volunteer to Review + +- Anyone in the community can volunteer as a Bioconductor community reviewer; no special credentials required. +- Before signing up: read the Review Expectations chapter and the Bioconductor Code of Conduct. +- Sign up via the volunteer Google form (linked from the chapter). +- Volunteers are then assigned incoming packages from the New Submission Tracker. + +Source: https://contributions.bioconductor.org/reviewer-resources-overview.html , https://contributions.bioconductor.org/review-expectation.html , https://contributions.bioconductor.org/reviewtools.html , https://contributions.bioconductor.org/review-volunteer-chapter.html +Fetched 2026-07-23 from contributions.bioconductor.org (Bioconductor devel guide). diff --git a/knowledge/workflow.md b/knowledge/workflow.md new file mode 100644 index 0000000..e5fb727 --- /dev/null +++ b/knowledge/workflow.md @@ -0,0 +1,151 @@ +# End-to-end submission runbook + +The sequential path from local package to first Bioconductor release. The per-chapter files +under this directory are reference; this file is the process. Follow it top to bottom. + +## Converting existing work (start here whenever code already exists) + +Most submitters are not starting from an empty directory. Two different starting points, and the +first thing to do is work out which one you are at - the advice diverges immediately. + +**A package already** (there is a `DESCRIPTION`): skip to the numbered list below. Do not scaffold +from scratch; you would overwrite metadata you already have. + +**Scripts, not a package** (no `DESCRIPTION` - analysis code, a bag of `.R` files, a repo of +notebooks): scaffolding is exactly right here, and it comes first. Do this, then join the list at +step 1. + +- **Settle the type before writing anything.** A pile of analysis code is often not a Software + package. If the point is to demonstrate an analysis using existing packages, it is a Workflow + package and the rules differ - no `man/`, `R/` or `data/` required. See + `development/non-software-pkgs.md` and `01-submissions.md`. Getting this wrong costs the most + and is the cheapest thing to check. +- **Create the package**, then run the biocthis chain in the tooling block in `AGENTS.md`. This is + the case that block was written for. +- **Turn top-level script code into functions.** Anything that runs at load time is a defect here: + no `setwd()`, no `rm(list = ls())`, no `install.packages()` or `library()` side effects, no + hardcoded paths. Paths become arguments. See `development/r-code.md`. +- **Decide what is exported.** Scripts have no public interface; a package is mostly interface. + Export the few functions a user calls, keep the rest internal, and document every export with + roxygen - man pages for exported objects are a gate item. +- **Find the data.** Scripts usually read local files that will not exist on the build machine. + Small examples go in `inst/extdata`; anything large goes to ExperimentHub/AnnotationHub. See + `development/data.md`. +- **Then the numbered list below**, starting at step 1. + +The numbered list is ordered by how expensive the problem is to discover late, not by how hard it +is to fix. + +1. **Eligibility and type** - Phase 0 below. Cheapest to answer and the only one that can end the + effort entirely. +2. **CRAN status** - "Not exist on CRAN. A package can only be submitted to one or the other." + Moving from CRAN means leaving CRAN, not dual-listing. Note also that the naming policy says a + name should not conflict with "any current or past CRAN package"; a maintainer migrating their + own package should raise that in the submission issue rather than assume it is fine. +3. **Name** - see `development/package-name.md`. Renaming after review has started is painful, and + the check is a search, not a build. +4. **Version** - reset to `0.99.0` no matter what the package is at today. A package at `2.4.1` on + GitHub still submits as `0.99.0`. See `maintenance.md`. +5. **Metadata gaps** - `biocViews` (usually missing entirely on a non-Bioc package), `Authors@R` + with a valid `cre` email, `NEWS.md`, `inst/CITATION`. The `biocthis::use_bioc_*()` chain + writes these; see the tooling block in `AGENTS.md`. One trap for a conversion: + `use_bioc_description()` replaces DESCRIPTION rather than merging into it, and declines + silently when it cannot ask - so on an existing package add `biocViews` by hand. +6. **Reuse audit** - does the package define its own container where `SummarizedExperiment`, + `GRanges`, or another core class would do? This is the single most common substantive review + request and the most expensive to retrofit. See `development/methods-classes.md` (ch 5). +7. **Data placement** - anything large moves out of the package to ExperimentHub/AnnotationHub + before you measure sizes. See `development/data.md`. +8. **Documentation** - a real evaluated vignette, not a stub; man pages with runnable examples. + Existing packages usually have a README doing the vignette's job. See + `development/documentation.md`. +9. **Code style** - `<-`, 4-space indent, 80 columns, no `1:n`. Mechanical, so do it last; doing it + first only creates conflicts with the changes above. +10. **Run the gate** - Phase 1 below, then Phase 2 onward unchanged. + +Steps 1-5 are usually a day. Step 6 is where a conversion either goes smoothly or becomes a +rewrite, so check it early even though it is fixed late. + +## Phase 0 - Decide it belongs in Bioconductor +- Package addresses high-throughput genomic / biological data analysis. +- Reuses standard Bioconductor data structures (e.g. SummarizedExperiment, S4) where possible. + See `development/methods-classes.md`. +- Not already on CRAN; CRAN/Bioc-only dependencies. +- Pick a type: Software, Experiment Data, Annotation, or Workflow. See `01-submissions.md`. + +## Phase 1 - Build to the gate (before you submit) +Author against the development chapters, then clear every item below. Detail: +`development/build-check-bioccheck.md`, `development/general-dev.md`, `development/metadata-files.md`. + +Tier 1 - stated as requirements: +- `R CMD check` clean on current R-devel (no errors, no warnings). +- `BiocCheck::BiocCheckGitClone()` clean. +- `BiocCheck::BiocCheck('new-package' = TRUE)` clean (no errors, no warnings). The tracker calls + passing check and BiocCheck "a minimum requirement for package acceptance", and notes that + passing "does not result in automatic acceptance" - review still follows. +- Every individual file <= 5 MB (upstream: "must be"). +- `biocViews` field present and valid; a vignette; man pages for exported objects. +- Valid maintainer email; maintainer == the person who will submit. + +Tier 2 - stated as should or recommended. Expected in practice and a reviewer will ask, but a +miss here is not a blocker: +- `Version: 0.99.0` in DESCRIPTION. See `maintenance.md` (version rule) and metadata-files. +- Source build under 10 MB (`R CMD build`); `R CMD check --no-build-vignettes` under 10 min. +- Running vignettes/examples/tests uses under 8 GB memory. +- Bioc code style in R code: `<-`, 4-space indent, 80-col. See `development/r-code.md`. + +Use the current devel Bioconductor with the matching R version - see `appendices.md` (Appendix A) +and `development/general-dev.md`. BiocCheck is the authoritative validator for everything above +except the two timing items, which need a real build; see the tooling block in `AGENTS.md`. + +## Phase 2 - Host on GitHub +- Push the package to the DEFAULT branch of a public GitHub repository (not a subdirectory, + not a non-default branch). +- Confirm `.gitignore` excludes build artifacts. See `development/gitignore.md`. + +## Phase 3 - Submit to the tracker +- Open a new issue at https://github.com/Bioconductor/Contributions/issues/new +- Issue TITLE = the package name. Body = link to your GitHub repo; confirm you have read the + guidelines and understand the review process. +- Annotation packages are the exception: email packages@bioconductor.org instead. +- Experiment Data that accompanies a software package: add to the same issue; submit the data + package first if the software depends on it. + +## Phase 4 - Single Package Builder (SPB) and review +- A webhook triggers the Single Package Builder; your package must build and check cleanly on + all platforms. Fix issues, push to GitHub, the build re-runs. +- A reviewer is assigned. Expect 2-6 weeks total. Respond within 2-3 weeks or the issue may be + closed for inactivity. +- On each change, bump the `z` in the version (0.99.0 -> 0.99.1 -> ...) and push. See + `maintenance.md` (version rule) and `reviewer.md` for what reviewers check. + +## Phase 5 - Acceptance and the Bioconductor git server +Once accepted (detail: `maintenance.md`, ch 24): +- Register your SSH public key at the BiocCredentials app + (https://git.bioconductor.org/BiocCredentials/). Bioconductor also reads keys from + https://github.com/.keys +- Add the Bioconductor remote and keep GitHub as origin: + - `git remote add upstream git@git.bioconductor.org:packages/.git` + - `git remote -v` should show origin (GitHub) and upstream (git.bioconductor.org) +- Sync then push to BOTH remotes: + - `git fetch --all` + - `git merge upstream/devel` (resolve conflicts if any) + - `git push upstream devel` (triggers the daily build, ~24h) and `git push origin devel` +- Only `devel` and the current `RELEASE_x_y` branches accept pushes. You cannot create new + branches on the Bioconductor server. Backport fixes with `git cherry-pick` onto the release + branch, then push that branch. + +## Phase 6 - Release and ongoing maintenance +- Bioconductor releases twice a year (around April and October). At the first release your + `0.99.z` becomes `1.0.0`. Devel and release version parity: `y` odd in devel, even in release. +- Monitor the daily/weekly build reports and fix breakages promptly. See `maintenance.md` + (troubleshooting build report, debugging C/C++). +- Subscribe to the bioc-devel mailing list; create a support.bioconductor.org account and watch + your package tag; keep the maintainer email in DESCRIPTION current. +- Deprecate/retire APIs per the lifecycle rules (`.Deprecated` -> `.Defunct` -> removed). See + `maintenance.md` (deprecation, end-of-life). + +Source: https://contributions.bioconductor.org/bioconductor-package-submissions.html, +https://contributions.bioconductor.org/git-version-control.html, +https://contributions.bioconductor.org/versionnum.html +Fetched 2026-08-14 from contributions.bioconductor.org (Bioconductor devel guide). diff --git a/scripts/golden-path.R b/scripts/golden-path.R new file mode 100644 index 0000000..fb60236 --- /dev/null +++ b/scripts/golden-path.R @@ -0,0 +1,235 @@ +#!/usr/bin/env Rscript + +## Build a throwaway package by running, verbatim, the scaffolding chain this repo tells users +## to run. Nothing here is a template: every file comes from biocthis. CI then puts the result +## through R CMD build, R CMD check and BiocCheck, so a green build means the instructions in +## AGENTS.md, SKILL.md and agents/bioc-package-review.md are instructions that were actually +## tested end to end, not instructions that merely look right. +## +## Usage: +## Rscript scripts/golden-path.R [output-directory] +## +## Default output directory is /tmp/GoldenPathPkg (override with GOLDEN_PATH_DIR). +## scripts/verify.py asserts that every use_bioc_*() call in the documented block appears here. + +options( + usethis.quiet = TRUE, + warn = 1, + repos = c(CRAN = "https://cloud.r-project.org") +) + +args <- commandArgs(trailingOnly = TRUE) +outdir <- if (length(args) >= 1) { + args[[1]] +} else { + Sys.getenv("GOLDEN_PATH_DIR", file.path(tempdir(), "GoldenPathPkg")) +} +pkg <- basename(outdir) + +## use_bioc_vignette() calls usethis::use_package(), which check_installed()s each Suggests it +## adds - so BiocStyle and friends must be present, not merely declared. Finding that out here +## with a clear message beats finding it out mid-chain. +deps <- c( + "usethis", "biocthis", "roxygen2", + "BiocStyle", "knitr", "RefManageR", "sessioninfo", "testthat", + "SummarizedExperiment" +) +for (dep in deps) { + if (!requireNamespace(dep, quietly = TRUE)) { + stop("golden-path needs ", dep, ": BiocManager::install(\"", dep, "\")", call. = FALSE) + } +} + +say <- function(...) cat("[golden-path]", ..., "\n", sep = " ") + +if (dir.exists(outdir)) { + say("removing previous", outdir) + unlink(outdir, recursive = TRUE) +} +dir.create(dirname(outdir), recursive = TRUE, showWarnings = FALSE) + +say("creating package", pkg, "in", outdir) +usethis::create_package(outdir, open = FALSE, rstudio = FALSE) +usethis::proj_set(outdir, force = TRUE) + +## A package with no code has nothing to document, and BiocCheck has opinions about that. One +## exported function, written in Bioconductor style (assignment arrow, four-space indent, +## seq_along rather than 1:n) so the example is not quietly teaching the wrong habits. +dir.create(file.path(outdir, "R"), showWarnings = FALSE) +writeLines( + c( + "#' Count observed values per sample", + "#'", + "#' @param se A [SummarizedExperiment::SummarizedExperiment].", + "#' @param assay_name Name or index of the assay to count.", + "#'", + "#' @return An integer vector with one element per column of `se`.", + "#'", + "#' @examples", + "#' library(SummarizedExperiment)", + "#' counts <- matrix(c(1, NA, 3, 4), nrow = 2)", + "#' se <- SummarizedExperiment(assays = list(counts = counts))", + "#' countObserved(se)", + "#'", + "#' @importFrom SummarizedExperiment assay", + "#' @export", + "countObserved <- function(se, assay_name = 1L) {", + " mat <- SummarizedExperiment::assay(se, assay_name)", + " counts <- integer(ncol(mat))", + " for (i in seq_along(counts)) {", + " counts[[i]] <- sum(!is.na(mat[, i]))", + " }", + " names(counts) <- colnames(mat)", + " counts", + "}" + ), + file.path(outdir, "R", "countObserved.R") +) + +## Unit tests, because the guide asks for them and BiocCheck says so out loud ("Consider adding +## unit tests. We strongly encourage them."). A fixture that skips them is not modelling the +## package we tell people to submit. +dir.create(file.path(outdir, "tests", "testthat"), recursive = TRUE, showWarnings = FALSE) +writeLines( + c( + 'library(testthat)', + paste0('library(', pkg, ')'), + '', + paste0('test_check("', pkg, '")') + ), + file.path(outdir, "tests", "testthat.R") +) +writeLines( + c( + 'test_that("countObserved counts non-missing values per sample", {', + ' counts <- matrix(c(1, NA, 3, 4), nrow = 2)', + ' se <- SummarizedExperiment::SummarizedExperiment(', + ' assays = list(counts = counts)', + ' )', + ' expect_identical(countObserved(se), c(1L, 2L))', + '})', + '', + 'test_that("countObserved rejects a non-SummarizedExperiment", {', + ' expect_error(countObserved(1:3))', + '})' + ), + file.path(outdir, "tests", "testthat", "test-countObserved.R") +) + +## --------------------------------------------------------------------------------------- +## The documented chain. Keep these calls identical to the block in AGENTS.md, SKILL.md and +## agents/bioc-package-review.md - verify.py check 8 fails the build if they drift apart. +## --------------------------------------------------------------------------------------- + +## use_bioc_description() goes through usethis::write_over(), which will not replace an existing +## file without approval and declines silently when it cannot ask. create_package() has just +## written a DESCRIPTION, so leaving it in place would make the next call a silent no-op and the +## package would end up with no biocViews. Removing it is what "approve the overwrite" amounts +## to. On a real existing package the honest advice is the opposite: add biocViews by hand rather +## than let this discard your metadata. +unlink(desc_path <- file.path(outdir, "DESCRIPTION")) + +say("biocthis::use_bioc_description()") +biocthis::use_bioc_description(biocViews = "Software, GeneExpression, Transcriptomics") + +say("biocthis::use_bioc_news_md()") +biocthis::use_bioc_news_md(open = FALSE) + +say("biocthis::use_bioc_vignette()") +biocthis::use_bioc_vignette(name = pkg, title = paste("Introduction to", pkg)) + +say("biocthis::use_bioc_citation()") +biocthis::use_bioc_citation() + +say("biocthis::use_bioc_github_action()") +biocthis::use_bioc_github_action() + +## --------------------------------------------------------------------------------------- +## The two things the guide requires that biocthis does not decide for you. +## --------------------------------------------------------------------------------------- + +## BiocCheck warns that a scaffolded Description is "too concise" and that a Software package +## with no Bioconductor dependencies should consider CRAN. Both are real submission feedback, so +## the fixture answers them rather than suppressing them: a Description of several sentences, and +## a genuine SummarizedExperiment dependency - which is also the reuse rule this repo teaches. +say("writing a Description of substance and declaring the Bioconductor dependency") +desc <- readLines(desc_path) +desc <- desc[!grepl("^(Title|Description|Imports):", desc)] +desc <- c( + desc, + paste( + "Title: Count Observed Values Per Sample In A SummarizedExperiment" + ), + paste( + "Description: Counts the non-missing values in each column of an assay stored in a", + "SummarizedExperiment object. The result is one integer per sample, which is a common", + "first step when assessing coverage or sparsity across an experiment. This package", + "exists to exercise the scaffolding chain documented in AGENTS.md and is not intended", + "for analysis." + ), + "Imports: SummarizedExperiment" +) +writeLines(desc, desc_path) + +say("confirming Version: 0.99.0") +desc <- readLines(desc_path) +desc[grepl("^Version:", desc)] <- "Version: 0.99.0" +writeLines(desc, desc_path) + +## biocthis 1.23.0 ships inst/CITATION with an empty title: the template substitutes {{Title}} +## but use_bioc_citation() never passes one. citation() errors on an empty title, the generated +## vignette calls citation(), and so R CMD build fails at "creating vignettes". A real user has +## to fill this file in anyway - the DOI in it is the literal string 10.1101/TODO - so doing the +## minimum here is modelling the user, not papering over the bug. +say("filling in the CITATION fields that use_bioc_citation() leaves empty") +cit_path <- file.path(outdir, "inst", "CITATION") +cit <- readLines(cit_path) +holes <- c('title = ""', 'as.person("")') +if (!any(vapply(holes, function(h) any(grepl(h, cit, fixed = TRUE)), logical(1)))) { + warning( + "inst/CITATION has no empty title or author - biocthis may have fixed this. ", + "Re-check before keeping this workaround.", + call. = FALSE + ) +} +cit <- sub('title = ""', paste0('title = "', pkg, ': a golden-path fixture"'), cit, fixed = TRUE) +cit <- sub('as.person("")', 'as.person("Golden Path")', cit, fixed = TRUE) +writeLines(cit, cit_path) + +say("roxygenise (man pages for exported objects)") +roxygen2::roxygenise(outdir, load_code = roxygen2::load_source) + +## --------------------------------------------------------------------------------------- +## Assert the chain produced what the gate asks for. A silent rename upstream (a use_bioc_*() +## function that no longer writes NEWS.md, say) has to fail here rather than three steps later +## inside BiocCheck output nobody reads. +## --------------------------------------------------------------------------------------- + +expected <- c( + "DESCRIPTION", + "NAMESPACE", + "NEWS.md", + "inst/CITATION", + file.path("vignettes", paste0(pkg, ".Rmd")), + file.path("man", "countObserved.Rd"), + file.path("tests", "testthat.R") +) +missing <- expected[!file.exists(file.path(outdir, expected))] +if (length(missing) > 0L) { + stop( + "the documented scaffolding chain did not produce: ", + paste(missing, collapse = ", "), + call. = FALSE + ) +} + +desc <- readLines(desc_path) +if (!any(grepl("^Version: 0\\.99\\.0$", desc))) { + stop("DESCRIPTION is not at Version: 0.99.0", call. = FALSE) +} +if (!any(grepl("^biocViews:", desc))) { + stop("use_bioc_description() no longer writes a biocViews field", call. = FALSE) +} + +say("ok:", outdir) +cat(outdir, "\n", sep = "") diff --git a/scripts/verify.py b/scripts/verify.py new file mode 100644 index 0000000..7b0a451 --- /dev/null +++ b/scripts/verify.py @@ -0,0 +1,677 @@ +#!/usr/bin/env python3 +"""Verify the internal consistency of this repository. + +Layer 1 (default) is static: no network, no R, no LLM, Python 3 stdlib only. It checks the +things that rot silently in a repo made of prose - dead paths, stamps, duplicated rule text +that has drifted between the files that restate it. + +Layer 2 (--network) checks this repo against its upstreams. See check_network(). + + python3 scripts/verify.py + python3 scripts/verify.py --network + python3 scripts/verify.py --list + +Exit status is 1 if any check fails. Warnings never fail the run. +""" + +from __future__ import annotations + +import argparse +import datetime as dt +import json +import os +import re +import subprocess +import sys +from dataclasses import dataclass, field + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + +# Files that restate the pre-submission gate. Kept in sync deliberately (an agent needs the gate +# in context nearly every turn, so it is duplicated rather than referenced), which is exactly why +# it has to be checked. +GATE_FILES = [ + "AGENTS.md", + "skills/bioconductor-package-dev/SKILL.md", + "agents/bioc-package-review.md", + "knowledge/workflow.md", +] + +# The three router files that carry the identical BiocCheck/biocthis tooling block. +ROUTER_FILES = [ + "AGENTS.md", + "skills/bioconductor-package-dev/SKILL.md", + "agents/bioc-package-review.md", +] + +# Sections that must be byte-identical between AGENTS.md and SKILL.md. +SHARED_SECTIONS = [ + "## Pre-submission gate", + "## Version rule", + "## Bioconductor code style (differs from tidyverse)", +] + +# The five hard numbers of the gate. Every file restating the gate must carry all of them. +GATE_VALUES = ["0.99.0", "10 MB", "10 min", "5 MB", "8 GB"] + +# Tools this repo used to reimplement and must never reference again. +DEAD_REFERENCES = ["check-submission.R", "context/REFRESH.md"] + +# Top-level names that mean "a path inside this repository". Deleted and not-yet-created +# directories are listed on purpose: a reference to one must fail, not be silently skipped. +REPO_PREFIXES = { + ".claude-plugin", ".github", "agents", "context", "docs", "evals", + "knowledge", "scripts", "skills", "templates", +} + +# Basenames that name a file in the *user's* package, not in this repo. +FOREIGN_BASENAMES = {"NEWS.md", "README.md", "CITATION.md", "INSTALL.md"} + +# Spelled with escapes, not literals: a checker that trips its own check is not a good look, and +# box-drawing characters (U+2500-257F, used by the README tree) must stay outside these ranges. +EMOJI = re.compile( + "[" + "\U0001f000-\U0001faff" # pictographs, emoticons, transport, symbols + "\u2600-\u27bf" # miscellaneous symbols and dingbats + "\u2b00-\u2bff" # arrows and geometric shapes + "\ufe0f" # variation selector 16 + "]" +) + +SLUG_URL = re.compile(r"https://contributions\.bioconductor\.org/([A-Za-z0-9._-]+)\.html") + + +@dataclass +class Result: + failures: list[str] = field(default_factory=list) + warnings: list[str] = field(default_factory=list) + + def fail(self, msg: str) -> None: + self.failures.append(msg) + + def warn(self, msg: str) -> None: + self.warnings.append(msg) + + +def git(*args: str) -> str: + return subprocess.run( + ["git", *args], cwd=ROOT, capture_output=True, text=True, check=False + ).stdout + + +def tracked_files() -> list[str]: + return [p for p in git("ls-files").splitlines() if p] + + +def read(path: str) -> str: + with open(os.path.join(ROOT, path), encoding="utf-8") as handle: + return handle.read() + + +def shipped_markdown() -> list[str]: + """Every tracked markdown file. context/ is gitignored, so it is absent by construction.""" + return [p for p in tracked_files() if p.endswith(".md")] + + +def knowledge_files() -> list[str]: + return [ + p for p in tracked_files() + if p.startswith("knowledge/") and p.endswith(".md") and p != "knowledge/SOURCES.md" + ] + + +def section(text: str, heading: str) -> str | None: + """Return the body of a `## heading` section, up to the next heading of the same level.""" + lines = text.splitlines() + try: + start = lines.index(heading) + except ValueError: + return None + body = [] + for line in lines[start + 1:]: + if line.startswith("## "): + break + body.append(line) + return "\n".join(body).strip("\n") + + +# -------------------------------------------------------------------------------------------- +# Check 1 - plugin manifests +# -------------------------------------------------------------------------------------------- + +def check_manifests(res: Result) -> None: + try: + plugin = json.loads(read(".claude-plugin/plugin.json")) + market = json.loads(read(".claude-plugin/marketplace.json")) + except (OSError, json.JSONDecodeError) as exc: + res.fail(f"manifests: cannot parse - {exc}") + return + + for key in ("name", "description", "version"): + if not plugin.get(key): + res.fail(f".claude-plugin/plugin.json: missing required key {key!r}") + for key in ("name", "description", "owner", "plugins"): + if not market.get(key): + res.fail(f".claude-plugin/marketplace.json: missing required key {key!r}") + + name = plugin.get("name") + if name and not os.path.isdir(os.path.join(ROOT, "skills", name)): + res.fail(f"plugin.json name {name!r} has no matching skills/{name}/ directory") + + listed = [p.get("name") for p in market.get("plugins", [])] + if name and name not in listed: + res.fail(f"plugin.json name {name!r} is not listed in marketplace.json plugins {listed}") + + version = plugin.get("version", "") + if not re.fullmatch(r"\d+\.\d+\.\d+", version): + res.fail(f"plugin.json version {version!r} is not x.y.z - users only update on a bump") + + +# -------------------------------------------------------------------------------------------- +# Check 2 - skill and agent frontmatter +# -------------------------------------------------------------------------------------------- + +def frontmatter(path: str) -> dict[str, str]: + text = read(path) + if not text.startswith("---\n"): + return {} + end = text.find("\n---\n", 4) + if end == -1: + return {} + block = text[4:end] + out: dict[str, str] = {} + key = None + for line in block.splitlines(): + m = re.match(r"^([A-Za-z_][A-Za-z0-9_-]*):\s*(.*)$", line) + if m: + key = m.group(1) + out[key] = m.group(2).strip() + elif key and line.strip(): + out[key] = (out[key] + " " + line.strip()).strip() + return out + + +def check_frontmatter(res: Result) -> None: + skill = "skills/bioconductor-package-dev/SKILL.md" + meta = frontmatter(skill) + if not meta: + res.fail(f"{skill}: no YAML frontmatter") + else: + if meta.get("name") != "bioconductor-package-dev": + res.fail(f"{skill}: frontmatter name {meta.get('name')!r} != directory name") + desc = meta.get("description", "") + if len(desc) < 80: + res.fail(f"{skill}: description is {len(desc)} chars - too thin to route on") + # The target user converts an existing package. If the description stops covering that + # vocabulary the skill silently stops firing for them while every other check passes. + for term in ("existing", "CRAN", "submission"): + if term.lower() not in desc.lower(): + res.fail(f"{skill}: description never mentions {term!r} (conversion audience)") + + agent = "agents/bioc-package-review.md" + meta = frontmatter(agent) + if not meta: + res.fail(f"{agent}: no YAML frontmatter") + return + for key in ("name", "description", "model", "tools"): + if not meta.get(key): + res.fail(f"{agent}: frontmatter missing {key!r}") + expected = os.path.basename(agent)[:-3] + if meta.get("name") != expected: + res.fail(f"{agent}: frontmatter name {meta.get('name')!r} != filename {expected!r}") + if "Write" in meta.get("tools", "") or "Edit" in meta.get("tools", ""): + res.fail(f"{agent}: grants write tools, but the agent is specified as read-only") + + +# -------------------------------------------------------------------------------------------- +# Check 3 - path references resolve and are not gitignored +# -------------------------------------------------------------------------------------------- + +BACKTICKED = re.compile(r"`([^`\n]+)`") + + +def path_candidates(path: str, line: str) -> list[str]: + """Backticked tokens on one line that are meant to name a file in *this* repository. + + Most backticked paths in these documents belong to the user's package (`tests/`, `man/`, + `inst/CITATION`) or to an upstream project, so the filter has to be narrow: a token counts + only if it is rooted at one of this repo's top-level directories, or if it is a relative + markdown reference inside knowledge/. Lines carrying a URL are skipped entirely - they are + naming somebody else's file. + """ + if "http" in line: + return [] + out = [] + for token in BACKTICKED.findall(line): + token = token.strip().replace("${CLAUDE_PLUGIN_ROOT}/", "").rstrip(".,;:") + if not token or " " in token or "(" in token or token.startswith(("<", "-")): + continue + if os.path.basename(token) in FOREIGN_BASENAMES: + continue + rooted = token.split("/", 1)[0] in REPO_PREFIXES + knowledge_ref = path.startswith("knowledge/") and token.endswith(".md") + if rooted or knowledge_ref: + out.append(token) + return out + + +def resolve(path: str, token: str, by_basename: dict[str, list[str]]) -> str | None: + for base in (ROOT, os.path.join(ROOT, os.path.dirname(path)), os.path.join(ROOT, "knowledge")): + if os.path.exists(os.path.join(base, token)): + return os.path.relpath(os.path.join(base, token), ROOT) + if "/" not in token: + hits = by_basename.get(token, []) + if len(hits) == 1: + return hits[0] + return None + + +def check_paths(res: Result) -> None: + by_basename: dict[str, list[str]] = {} + for tracked in tracked_files(): + by_basename.setdefault(os.path.basename(tracked), []).append(tracked) + + ignored: dict[str, bool] = {} + for path in shipped_markdown(): + for lineno, line in enumerate(read(path).splitlines(), 1): + for token in path_candidates(path, line): + resolved = resolve(path, token, by_basename) + if resolved is None: + res.fail(f"{path}:{lineno}: references `{token}`, which does not exist") + continue + if resolved not in ignored: + code = subprocess.run( + ["git", "check-ignore", "-q", resolved], + cwd=ROOT, capture_output=True, check=False, + ).returncode + ignored[resolved] = code == 0 + if ignored[resolved]: + res.fail( + f"{path}:{lineno}: references `{token}`, which is gitignored - " + "dead path in every clone and in the installed plugin" + ) + + +# -------------------------------------------------------------------------------------------- +# Check 4 - README layout tree matches the repository +# -------------------------------------------------------------------------------------------- + +def check_readme_tree(res: Result) -> None: + text = read("README.md") + body = section(text, "## Repository layout") + if body is None: + res.fail("README.md: no '## Repository layout' section") + return + block = re.search(r"```\n(.*?)```", body, re.S) + if not block: + res.fail("README.md: layout section has no fenced tree") + return + + # Only depth-0 entries are compared: a top-level directory disappearing is the failure that + # matters, and mirroring every leaf would make the tree a second source of truth. + listed = set() + for line in block.group(1).splitlines(): + m = re.match(r"^(?:├── |└── )(\S+)", line) + if not m: + continue + listed.add(m.group(1).rstrip("/").split("/")[0]) + + actual = {p.split("/")[0] for p in tracked_files()} + actual.discard(".gitignore") + missing = sorted(actual - listed) + if missing: + res.fail(f"README.md layout tree omits tracked top-level entries: {missing}") + + for entry in sorted(listed - actual): + if entry.startswith("."): + continue + res.warn(f"README.md layout tree lists {entry!r}, which is not tracked yet") + + +# -------------------------------------------------------------------------------------------- +# Check 5 - Source and Fetched stamps +# -------------------------------------------------------------------------------------------- + +STAMP = re.compile(r"^Fetched (\d{4}-\d{2}-\d{2})", re.M) + + +def check_stamps(res: Result) -> None: + today = dt.date.today() + for path in knowledge_files(): + text = read(path) + if not SLUG_URL.search(text): + res.fail(f"{path}: no canonical contributions.bioconductor.org Source: URL") + m = STAMP.search(text) + if not m: + res.fail(f"{path}: no 'Fetched YYYY-MM-DD' stamp") + continue + age = (today - dt.date.fromisoformat(m.group(1))).days + if age > 90: + res.warn(f"{path}: Fetched stamp is {age} days old") + + +# -------------------------------------------------------------------------------------------- +# Check 6 - SOURCES.md map and the per-file Source: footers agree +# -------------------------------------------------------------------------------------------- + +def sources_rows() -> list[tuple[str, str, str]]: + """(rmd, slug, target) rows of the chapter map in knowledge/SOURCES.md.""" + rows = [] + for line in read("knowledge/SOURCES.md").splitlines(): + if not line.startswith("| `") or line.startswith("| ---"): + continue + cells = [c.strip() for c in line.strip("|").split("|")] + if len(cells) != 4: + continue + rmd, slug, _chapter, target = cells + rows.append((rmd.strip("`"), slug.strip("`"), target)) + return rows + + +def check_sources_map(res: Result) -> None: + rows = sources_rows() + if not rows: + res.fail("knowledge/SOURCES.md: chapter map has no parseable rows") + return + + mapped_slugs = {slug for _rmd, slug, _t in rows if slug != "-"} + + # Forward: every mapped slug whose target is a real file must be cited by that file. + for _rmd, slug, target in rows: + if slug == "-" or not target.startswith("`"): + continue + target_path = os.path.join("knowledge", target.strip("`")) + if not os.path.exists(os.path.join(ROOT, target_path)): + res.fail(f"knowledge/SOURCES.md: maps {slug} to {target_path}, which does not exist") + continue + if slug not in SLUG_URL.findall(read(target_path)): + res.fail( + f"knowledge/SOURCES.md: maps slug {slug!r} to {target_path}, " + f"but that file never cites {slug}.html in its Source: footer" + ) + + # Reverse: every slug cited anywhere must be in the map. + for path in knowledge_files(): + for slug in set(SLUG_URL.findall(read(path))): + if slug not in mapped_slugs: + res.fail(f"{path}: cites slug {slug!r}, which is absent from the SOURCES.md map") + + +# -------------------------------------------------------------------------------------------- +# Check 7 - references to tooling this repo deliberately removed +# -------------------------------------------------------------------------------------------- + +def check_dead_references(res: Result) -> None: + for path in shipped_markdown(): + text = read(path) + for needle in DEAD_REFERENCES: + if needle in text: + res.fail(f"{path}: references {needle!r}, which was removed from this repo") + if re.search(r"`templates/", text): + res.fail( + f"{path}: references `templates/`, which was removed - " + "point at biocthis instead" + ) + + +# -------------------------------------------------------------------------------------------- +# Check 8 - the BiocCheck/biocthis block is identical everywhere it appears +# -------------------------------------------------------------------------------------------- + +TOOLING_HEADING = "## Tooling (use these, do not reimplement them)" + + +def tooling_block(text: str) -> str | None: + for m in re.finditer(r"```r\n(.*?)```", text, re.S): + if "BiocCheckGitClone" in m.group(1): + return m.group(1) + return None + + +def check_tooling_block(res: Result) -> None: + blocks = {} + for path in ROUTER_FILES: + text = read(path) + if TOOLING_HEADING not in text: + res.fail(f"{path}: missing the {TOOLING_HEADING!r} section") + block = tooling_block(text) + if block is None: + res.fail(f"{path}: has no BiocCheck/biocthis command block") + continue + blocks[path] = block + + if len(set(blocks.values())) > 1: + res.fail( + "the BiocCheck/biocthis command block differs between " + + ", ".join(sorted(blocks)) + + " - all three must be identical, and golden-path.R runs it" + ) + + for path, block in blocks.items(): + if "BiocCheckGitClone()" not in block or "'new-package' = TRUE" not in block: + res.fail(f"{path}: tooling block does not call both BiocCheck entry points") + if "use_bioc_description" not in block: + res.fail(f"{path}: tooling block does not scaffold with biocthis") + + golden = os.path.join(ROOT, "scripts", "golden-path.R") + if blocks and os.path.exists(golden): + with open(golden, encoding="utf-8") as handle: + script = handle.read() + for call in sorted(set(re.findall(r"use_bioc_\w+", next(iter(blocks.values()))))): + if call not in script: + res.fail( + f"scripts/golden-path.R never calls {call}(), " + "so the documented scaffolding chain is not the tested one" + ) + + +# -------------------------------------------------------------------------------------------- +# Check 9 - the gate numbers appear everywhere the gate is restated +# -------------------------------------------------------------------------------------------- + +def check_gate_values(res: Result) -> None: + for path in GATE_FILES: + text = read(path) + for value in GATE_VALUES: + if value not in text: + res.fail(f"{path}: restates the gate but never mentions {value!r}") + + +# -------------------------------------------------------------------------------------------- +# Check 10 - no emoji +# -------------------------------------------------------------------------------------------- + +def check_no_emoji(res: Result) -> None: + for path in tracked_files(): + if not path.endswith((".md", ".py", ".R", ".json", ".yml", ".yaml")): + continue + for lineno, line in enumerate(read(path).splitlines(), 1): + m = EMOJI.search(line) + if m: + res.fail(f"{path}:{lineno}: emoji {m.group(0)!r} - repo docs are plain text") + + +# -------------------------------------------------------------------------------------------- +# Check 11 - duplicated rule text has not drifted, and the router lives in one place +# -------------------------------------------------------------------------------------------- + +def check_single_source(res: Result) -> None: + agents = read("AGENTS.md") + skill = read("skills/bioconductor-package-dev/SKILL.md") + + for heading in SHARED_SECTIONS: + a, s = section(agents, heading), section(skill, heading) + if a is None: + res.fail(f"AGENTS.md: missing shared section {heading!r}") + if s is None: + res.fail(f"SKILL.md: missing shared section {heading!r}") + if a is not None and s is not None and a != s: + res.fail( + f"section {heading!r} differs between AGENTS.md and SKILL.md - " + "these are duplicated deliberately and must stay byte-identical" + ) + + # The router is AGENTS.md's job. A parallel copy in SKILL.md is what drifts. + for lineno, line in enumerate(skill.splitlines(), 1): + if line.startswith("- ") and "knowledge/" in line: + res.fail( + f"SKILL.md:{lineno}: router-style bullet pointing into knowledge/ - " + "the router belongs in AGENTS.md only" + ) + + +# -------------------------------------------------------------------------------------------- +# Check 12 - the Fetched stamp is a contract, not a decoration +# -------------------------------------------------------------------------------------------- + +def check_stamp_contract(res: Result) -> None: + for path in knowledge_files(): + m = STAMP.search(read(path)) + if not m: + continue # already reported by check 5 + committed = git("log", "-1", "--format=%cs", "--", path).strip() + if not committed: + continue # never committed; nothing to compare against + if dt.date.fromisoformat(committed) > dt.date.fromisoformat(m.group(1)): + res.fail( + f"{path}: last commit {committed} is newer than its Fetched stamp {m.group(1)} - " + "content was edited without restamping, so the provenance is a claim it cannot back" + ) + + +# -------------------------------------------------------------------------------------------- +# Check 13 - the CI workflow pins the action version recorded in SOURCES.md +# -------------------------------------------------------------------------------------------- + +WORKFLOW = ".github/workflows/verify.yml" + + +def check_workflow_pins(res: Result) -> None: + if not os.path.exists(os.path.join(ROOT, WORKFLOW)): + res.fail(f"{WORKFLOW} does not exist - the verification layers are not wired to CI") + return + text = read(WORKFLOW) + + for m in re.finditer(r"uses:\s*(\S+)@(\S+)", text): + action, ref = m.groups() + if ref in ("main", "master", "devel", "HEAD"): + res.fail(f"{WORKFLOW}: {action} is pinned to a branch ({ref}), not a tag") + + pinned = re.search(r"bioc-actions.*?\|\s*`(v[\d.]+)`", read("knowledge/SOURCES.md")) + if not pinned: + res.fail("knowledge/SOURCES.md: no bioc-actions tag pin found") + return + used = set(re.findall(r"grimbough/bioc-actions/\S+@(\S+)", text)) + wrong = sorted(used - {pinned.group(1)}) + if wrong: + res.fail( + f"{WORKFLOW} uses bioc-actions {wrong}, but knowledge/SOURCES.md pins " + f"{pinned.group(1)} - bump both together or neither" + ) + + +# -------------------------------------------------------------------------------------------- +# Check 14 - documented example prompts are the prompts CI actually exercises +# -------------------------------------------------------------------------------------------- + +def eval_cases() -> list[str]: + return [p for p in tracked_files() if p.startswith("evals/") and p.endswith("case.yaml")] + + +def check_evals(res: Result) -> None: + cases = eval_cases() + if not cases: + res.fail("evals/ has no case.yaml files - Layer 4 is not wired up") + return + + for path in cases: + text = read(path) + name = os.path.basename(os.path.dirname(path)) + if "schema_version:" not in text: + res.fail(f"{path}: no schema_version - the eval runner rejects the case") + m = re.search(r"^name:\s*(\S+)", text, re.M) + if not m: + res.fail(f"{path}: no name field") + elif m.group(1) != name: + res.fail(f"{path}: name {m.group(1)!r} does not match its directory {name!r}") + if "graders:" not in text: + res.fail(f"{path}: no graders - a case with no grader scores nothing") + + # Every prompt the README advertises has to be one the suite actually runs, or the docs + # drift away from what is tested and the examples become folklore. + body = section(read("README.md"), "## Example prompts") or "" + documented = re.findall(r'^- "([^"]+)"', body, re.M) + if not documented: + res.fail("README.md: no quoted prompts found under '## Example prompts'") + return + + haystack = " ".join(re.sub(r"\s+", " ", read(p)) for p in cases) + for prompt in documented: + if re.sub(r"\s+", " ", prompt) not in haystack: + res.fail( + f"README.md documents the prompt {prompt!r}, which no eval case runs - " + "add a case for it or drop it from the README" + ) + + +STATIC_CHECKS = [ + ("manifests", check_manifests), + ("frontmatter", check_frontmatter), + ("paths", check_paths), + ("readme-tree", check_readme_tree), + ("stamps", check_stamps), + ("sources-map", check_sources_map), + ("dead-references", check_dead_references), + ("tooling-block", check_tooling_block), + ("gate-values", check_gate_values), + ("no-emoji", check_no_emoji), + ("single-source", check_single_source), + ("stamp-contract", check_stamp_contract), + ("workflow-pins", check_workflow_pins), + ("evals", check_evals), +] + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + parser.add_argument("--network", action="store_true", help="also run upstream fidelity checks") + parser.add_argument("--list", action="store_true", help="list checks and exit") + parser.add_argument("--json", action="store_true", help="emit machine-readable results") + args = parser.parse_args(argv) + + checks = list(STATIC_CHECKS) + if args.network: + from verify_network import NETWORK_CHECKS # noqa: PLC0415 (optional, needs network) + checks += NETWORK_CHECKS + + if args.list: + for name, fn in checks: + print(f"{name:18s} {(fn.__doc__ or '').strip().splitlines()[0] if fn.__doc__ else ''}") + return 0 + + res = Result() + for name, fn in checks: + before = len(res.failures) + try: + fn(res) + except Exception as exc: # a checker crash is a failure, not a pass + res.fail(f"{name}: checker raised {type(exc).__name__}: {exc}") + status = "FAIL" if len(res.failures) > before else "ok" + if not args.json: + print(f"[{status:4s}] {name}") + + if args.json: + print(json.dumps({"failures": res.failures, "warnings": res.warnings}, indent=2)) + else: + for msg in res.warnings: + print(f" warn: {msg}") + for msg in res.failures: + print(f" FAIL: {msg}") + print(f"\n{len(res.failures)} failure(s), {len(res.warnings)} warning(s)") + + return 1 if res.failures else 0 + + +if __name__ == "__main__": + sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + sys.exit(main()) diff --git a/scripts/verify_network.py b/scripts/verify_network.py new file mode 100644 index 0000000..4b5984b --- /dev/null +++ b/scripts/verify_network.py @@ -0,0 +1,442 @@ +#!/usr/bin/env python3 +"""Layer 2: check this repository against the upstreams it summarizes. + +Imported by verify.py when run with --network; not meant to be run on its own. Every check here +needs outbound HTTPS and nothing else - no R, no LLM, no API token. In CI these run on a weekly +cron rather than as a pull-request gate, because upstream changing is a reason to open an issue, +not a reason to block somebody's pull request. + +The fidelity checks work by quotation. Where a knowledge file states a load-bearing rule, it +quotes upstream's own words for that rule, and the check asserts the quote is still present in +both places. That catches the two failures a link checker cannot see: upstream changing a rule, +and a summary restating a recommendation as a requirement. +""" + +from __future__ import annotations + +import html +import json +import os +import re +import ssl +import subprocess +import urllib.error +import urllib.request + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +GUIDE = "https://contributions.bioconductor.org" +USER_AGENT = "bioc_package_dev-verify/1.0 (+https://github.com/ybaeus/bioc_package_dev)" + +# A machine sitting behind a TLS-intercepting proxy has a trust store that urllib rejects and +# curl accepts, which would otherwise make every check here fail for a reason that has nothing +# to do with this repository. So: urllib first, curl second, and never -k in either. +_CURL_ENV = {k: v for k, v in os.environ.items() if k not in ("CURL_CA_BUNDLE", "SSL_CERT_FILE")} + +_cache: dict[str, str] = {} + + +def _curl(url: str, timeout: int, head: bool = False) -> tuple[int, str]: + args = ["curl", "-sS", "--max-time", str(timeout), "-A", USER_AGENT] + args += ["-o", os.devnull, "-w", "%{http_code}", "-I"] if head else ["-w", "\n%{http_code}"] + proc = subprocess.run( + [*args, url], capture_output=True, text=True, env=_CURL_ENV, check=False + ) + if proc.returncode != 0: + return 0, "" + if head: + return int(proc.stdout.strip() or 0), "" + body, _, code = proc.stdout.rpartition("\n") + return int(code.strip() or 0), body + + +def fetch(url: str, timeout: int = 30) -> str: + if url in _cache: + return _cache[url] + req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT}) + try: + with urllib.request.urlopen(req, timeout=timeout) as resp: # noqa: S310 - https only + body = resp.read().decode("utf-8", errors="replace") + except (ssl.SSLError, urllib.error.URLError) as exc: + code, body = _curl(url, timeout) + if code != 200: + raise OSError(f"{url}: urllib said {exc}; curl said HTTP {code}") from exc + _cache[url] = body + return body + + +def fetch_json(url: str) -> object: + return json.loads(fetch(url)) + + +def status(url: str, timeout: int = 30) -> int: + req = urllib.request.Request(url, method="HEAD", headers={"User-Agent": USER_AGENT}) + try: + with urllib.request.urlopen(req, timeout=timeout) as resp: # noqa: S310 + return resp.status + except urllib.error.HTTPError as exc: + return exc.code + except OSError: + return _curl(url, timeout, head=True)[0] + + +def strip_html(page: str) -> str: + page = re.sub(r"(?s)<(script|style).*?", " ", page) + page = re.sub(r"(?s)<[^>]+>", " ", page) + return html.unescape(page) + + +def normalize(text: str) -> str: + """Fold the differences that are not meaning: markup, quote glyphs, wrapping, case.""" + text = text.replace("**", "").replace("`", "").replace("_", "") + text = text.replace("’", "'").replace("‘", "'") + text = text.replace("“", '"').replace("”", '"') + text = text.replace("–", "-").replace("—", "-").replace(" ", " ") + return re.sub(r"\s+", " ", text).strip().lower() + + +def read(path: str) -> str: + with open(os.path.join(ROOT, path), encoding="utf-8") as handle: + return handle.read() + + +def chapter_text(slug: str) -> str: + return normalize(strip_html(fetch(f"{GUIDE}/{slug}.html"))) + + +# -------------------------------------------------------------------------------------------- +# The quotation table - the heart of the fidelity layer +# -------------------------------------------------------------------------------------------- +# Each row: the upstream chapter, the exact upstream wording, and the file in this repo that is +# required to carry that wording verbatim. Hand-maintained on purpose. Automatic sentence +# alignment between a chapter and its summary is unreliable, and a check nobody trusts is a check +# nobody triages - so this covers only the rules that carry weight. + +CLAIMS: list[dict[str, str]] = [ + { + "slug": "general", + "quote": "individual files must be <= 5MB", + "file": "knowledge/development/general-dev.md", + "note": "the only size limit upstream states as a requirement", + }, + { + "slug": "general", + "quote": "should occupy less than 10 MB on disk", + "file": "knowledge/development/general-dev.md", + "note": "source build size - a recommendation, not a blocker", + }, + { + "slug": "general", + "quote": "should require less than 10 minutes to run R CMD check", + "file": "knowledge/development/general-dev.md", + "note": "check duration - a recommendation, not a blocker", + }, + { + "slug": "general", + "quote": ( + "it is recommended that the vignettes, man page examples, and unit tests do not " + "require more than 8 GB of memory" + ), + "file": "knowledge/development/general-dev.md", + "note": "memory ceiling - explicitly a recommendation", + }, + { + "slug": "versionnum", + "quote": "should set version: 0.99.0 in the description file", + "file": "knowledge/maintenance.md", + "note": "the version every new submission starts at", + }, + { + "slug": "bioconductor-package-submissions", + "quote": "the default branch must contain only package code", + "file": "knowledge/01-submissions.md", + "note": "a requirement", + }, + { + "slug": "bioconductor-package-submissions", + "quote": "should be in a different branch", + "file": "knowledge/01-submissions.md", + "note": "CI helper files - a recommendation, and previously over-hardened here", + }, + { + "slug": "bioconductor-package-submissions", + "quote": "a package can only be submitted to one or the other", + "file": "knowledge/01-submissions.md", + "note": "CRAN and Bioconductor are mutually exclusive", + }, + { + "slug": "bioconductor-package-submissions", + "quote": "to submit a package to bioconductor the package should", + "file": "knowledge/01-submissions.md", + "note": "the eligibility list is stated as should, not must", + }, +] + +# The tracker's issue template, not our prose, is the authority on what submission requires. +TRACKER_TEMPLATE = ( + "https://raw.githubusercontent.com/Bioconductor/Contributions/devel/issue_template.md" +) +TRACKER_QUOTES = [ + ( + "a minimum requirement for package acceptance", + ["AGENTS.md", "skills/bioconductor-package-dev/SKILL.md"], + ), + ( + "does not result in automatic acceptance", + ["AGENTS.md", "skills/bioconductor-package-dev/SKILL.md"], + ), +] + + +# -------------------------------------------------------------------------------------------- +# Pins recorded in knowledge/SOURCES.md +# -------------------------------------------------------------------------------------------- + +def pins() -> dict[str, str]: + out: dict[str, str] = {} + for line in read("knowledge/SOURCES.md").splitlines(): + if not line.startswith("| ["): + continue + cells = [c.strip() for c in line.strip("|").split("|")] + if len(cells) < 2: + continue + name, pin = cells[0], cells[1] + sha = re.search(r"`([0-9a-f]{40})`", pin) + tag = re.search(r"`(v[\d.]+)`", pin) + rel = re.search(r"release ([\d.]+)", pin) + dev = re.search(r"devel ([\d.]+)", pin) + if "pkgrevdocs" in name and sha: + out["pkgrevdocs"] = sha.group(1) + elif "Contributions" in name and sha: + out["contributions"] = sha.group(1) + elif "BiocCheck" in name: + if rel: + out["bioccheck_release"] = rel.group(1) + if dev: + out["bioccheck_devel"] = dev.group(1) + elif "biocthis" in name: + if rel: + out["biocthis_release"] = rel.group(1) + if dev: + out["biocthis_devel"] = dev.group(1) + elif "bioc-actions" in name and tag: + out["bioc_actions"] = tag.group(1) + return out + + +def mapped_slugs() -> dict[str, str]: + """slug -> knowledge file (or a 'not summarized' note), from the SOURCES.md chapter map.""" + out = {} + for line in read("knowledge/SOURCES.md").splitlines(): + if not line.startswith("| `"): + continue + cells = [c.strip() for c in line.strip("|").split("|")] + if len(cells) != 4: + continue + _rmd, slug, _ch, target = cells + slug = slug.strip("`") + if slug != "-": + out[slug] = target + return out + + +def rmd_to_slugs() -> dict[str, list[str]]: + out: dict[str, list[str]] = {} + for line in read("knowledge/SOURCES.md").splitlines(): + if not line.startswith("| `"): + continue + cells = [c.strip() for c in line.strip("|").split("|")] + if len(cells) != 4: + continue + rmd, slug, _ch, target = cells + out.setdefault(rmd.strip("`"), []).append(f"{slug.strip('`')} -> {target}") + return out + + +# -------------------------------------------------------------------------------------------- +# Checks +# -------------------------------------------------------------------------------------------- + +def check_commit_drift(res) -> None: + """pkgrevdocs has moved: report which summaries the changed chapters map to.""" + pinned = pins().get("pkgrevdocs") + if not pinned: + res.fail("knowledge/SOURCES.md: no pkgrevdocs commit pin found") + return + head = fetch_json("https://api.github.com/repos/Bioconductor/pkgrevdocs/commits/devel") + current = head["sha"] # type: ignore[index] + if current == pinned: + return + + compare = fetch_json( + f"https://api.github.com/repos/Bioconductor/pkgrevdocs/compare/{pinned}...{current}" + ) + changed = [f["filename"] for f in compare.get("files", [])] # type: ignore[union-attr] + table = rmd_to_slugs() + affected = sorted({t for name in changed for t in table.get(name, [])}) + res.fail( + "pkgrevdocs moved from {} to {}. Changed files: {}. Affected summaries: {}. " + "Compare: https://github.com/Bioconductor/pkgrevdocs/compare/{}...{}".format( + pinned[:7], current[:7], + ", ".join(changed) or "(none reported)", + ", ".join(affected) or "(none mapped - check for a new chapter)", + pinned, current, + ) + ) + + +def check_url_liveness(res) -> None: + """Every canonical chapter URL cited by a summary still resolves.""" + cited = set() + for dirpath, _dirs, names in os.walk(os.path.join(ROOT, "knowledge")): + for name in names: + if not name.endswith(".md"): + continue + rel = os.path.relpath(os.path.join(dirpath, name), ROOT) + cited.update(re.findall(rf"{GUIDE}/([A-Za-z0-9._-]+)\.html", read(rel))) + for slug in sorted(cited): + code = status(f"{GUIDE}/{slug}.html") + if code != 200: + res.fail(f"{GUIDE}/{slug}.html returned {code} - chapter renamed or removed") + + +def check_quotations(res) -> None: + """Load-bearing rules are quoted verbatim from upstream, in both places.""" + for claim in CLAIMS: + quote = normalize(claim["quote"]) + try: + upstream = chapter_text(claim["slug"]) + except OSError as exc: + res.fail(f"cannot fetch {claim['slug']}.html: {exc}") + continue + if quote not in upstream: + res.fail( + f"upstream {claim['slug']}.html no longer contains {claim['quote']!r} " + f"({claim['note']}) - re-read the chapter before touching {claim['file']}" + ) + continue + if quote not in normalize(read(claim["file"])): + res.fail( + f"{claim['file']} must quote upstream verbatim: {claim['quote']!r} " + f"({claim['note']}). Paraphrasing here is how a recommendation " + "silently becomes a requirement." + ) + + +def check_tracker_gate(res) -> None: + """The submission tracker's checklist, not our prose, defines the gate.""" + pinned = pins().get("contributions") + latest = fetch_json( + "https://api.github.com/repos/Bioconductor/Contributions/commits" + "?path=issue_template.md&per_page=1" + ) + current = latest[0]["sha"] # type: ignore[index] + if pinned and current != pinned: + res.fail( + f"Contributions/issue_template.md moved from {pinned[:7]} to {current[:7]}. " + "This template is the authoritative submission checklist - read the diff in full " + "and update the gate in AGENTS.md, SKILL.md and agents/bioc-package-review.md. " + "https://github.com/Bioconductor/Contributions/commits/devel/issue_template.md" + ) + + template = normalize(fetch(TRACKER_TEMPLATE)) + for quote, files in TRACKER_QUOTES: + if normalize(quote) not in template: + res.fail(f"issue_template.md no longer says {quote!r} - the gate wording is stale") + continue + for path in files: + if normalize(quote) not in normalize(read(path)): + res.fail(f"{path} does not carry the tracker's own wording {quote!r}") + + +def check_chapter_coverage(res) -> None: + """A chapter added upstream is invisible to every per-file check, so diff the whole list.""" + index = fetch(f"{GUIDE}/index.html") + upstream = [] + for m in re.finditer(r'href="([A-Za-z0-9._-]+)\.html"', index): + if m.group(1) not in upstream: + upstream.append(m.group(1)) + known = mapped_slugs() + for slug in upstream: + if slug not in known: + res.fail( + f"upstream chapter {slug}.html is absent from the SOURCES.md map - " + "either summarize it or record it as deliberately not summarized" + ) + for slug in known: + if slug not in upstream and slug != "index": + res.warn(f"SOURCES.md maps {slug}, which no longer appears in the guide TOC") + + +def check_tool_pins(res) -> None: + """BiocCheck, biocthis and bioc-actions releases move independently of the guide.""" + recorded = pins() + views = {} + for channel in ("release", "devel"): + pkg = None + for line in fetch(f"https://bioconductor.org/packages/{channel}/bioc/VIEWS").splitlines(): + if line.startswith("Package:"): + pkg = line.split(":", 1)[1].strip() + elif line.startswith("Version:") and pkg in ("BiocCheck", "biocthis"): + views[f"{pkg.lower()}_{channel}"] = line.split(":", 1)[1].strip() + + for key, label in ( + ("bioccheck_release", "BiocCheck release"), + ("bioccheck_devel", "BiocCheck devel"), + ("biocthis_release", "biocthis release"), + ("biocthis_devel", "biocthis devel"), + ): + current = views.get(key) + if current and recorded.get(key) and current != recorded[key]: + res.warn( + f"{label} is now {current}, pinned at {recorded[key]} in knowledge/SOURCES.md" + ) + + tags = fetch_json("https://api.github.com/repos/grimbough/bioc-actions/tags?per_page=1") + if tags: + newest = tags[0]["name"] # type: ignore[index] + if recorded.get("bioc_actions") and newest != recorded["bioc_actions"]: + res.warn( + f"bioc-actions latest tag is {newest}, pinned at {recorded['bioc_actions']} - " + "read the action.yml diff before bumping .github/workflows/verify.yml" + ) + + +def check_bioc_cycle(res) -> None: + """The release/devel/R triple goes stale twice a year, and an agent will invent one.""" + config = fetch("https://bioconductor.org/config.yaml") + wanted = {} + for key in ("release_version", "devel_version", "r_version_associated_with_devel"): + m = re.search(rf'^{key}:\s*"?([\d.]+)"?', config, re.M) + if m: + wanted[key] = m.group(1) + if len(wanted) != 3: + res.fail("bioconductor.org/config.yaml: could not read the release/devel/R versions") + return + + expected = ( + f"Bioconductor release {wanted['release_version']}, " + f"devel {wanted['devel_version']}, " + f"both on R {wanted['r_version_associated_with_devel']}" + ) + for path in ( + "AGENTS.md", + "skills/bioconductor-package-dev/SKILL.md", + "agents/bioc-package-review.md", + "knowledge/SOURCES.md", + ): + if normalize(expected) not in normalize(read(path)): + res.fail( + f"{path} does not record the current cycle ({expected}) - " + "an agent with no stated version will guess one" + ) + + +NETWORK_CHECKS = [ + ("commit-drift", check_commit_drift), + ("url-liveness", check_url_liveness), + ("quotations", check_quotations), + ("tracker-gate", check_tracker_gate), + ("chapter-coverage", check_chapter_coverage), + ("tool-pins", check_tool_pins), + ("bioc-cycle", check_bioc_cycle), +] diff --git a/skills/bioconductor-package-dev/SKILL.md b/skills/bioconductor-package-dev/SKILL.md new file mode 100644 index 0000000..92a27cc --- /dev/null +++ b/skills/bioconductor-package-dev/SKILL.md @@ -0,0 +1,132 @@ +--- +name: bioconductor-package-dev +description: >- + Guides you through turning existing R work - a package, or just a pile of analysis scripts - + into a Bioconductor submission: what the requirements are, what the pre-submission gate demands, + and how peer review works. Use when a package on GitHub is being prepared for Bioconductor, when + turning scripts or messy analysis code into a submittable package, when moving a package from + CRAN to Bioconductor, when asked whether a package is submission-ready, and for any Bioconductor + development, maintenance, or review work: DESCRIPTION/NAMESPACE/NEWS/biocViews/BiocCheck, + version numbering, vignettes and man pages, large data placement, the Contributions tracker, + git.bioconductor.org, and S4 or Bioconductor core classes such as SummarizedExperiment - even + when the user does not say "Bioconductor" explicitly but the package clearly targets it. +--- + +# Bioconductor package development + +Curated from the official guide "Bioconductor Packages: Development, Maintenance, and Peer +Review" (https://contributions.bioconductor.org). The detailed summaries live in +`${CLAUDE_PLUGIN_ROOT}/knowledge/` - open the file for the task instead of loading everything. + +The common case is conversion: the user already has a working package or tool on GitHub and wants +it in Bioconductor. Start from what exists and find the gaps against the gate; do not scaffold +from scratch unless there is no package yet. + +## How to use this skill +1. Identify the lifecycle stage: Authoring, Conversion/Submission, Maintenance, or Review. +2. Open the matching `knowledge/` file for the rules and exact values. The topic router lives in + `${CLAUDE_PLUGIN_ROOT}/AGENTS.md` under "Router", and the full map in `knowledge/index.md`. +3. Apply the cross-cutting rules below (gate, version, style) to whatever you write or check. +4. For an end-to-end submission, follow `${CLAUDE_PLUGIN_ROOT}/knowledge/workflow.md`. For an + existing package, start at its "Converting an existing package" section. + +## Pre-submission gate +Two tiers, because upstream states them at two different strengths. Do not report a tier-2 item +as a blocker; report it as something a reviewer will very likely ask about. + +Tier 1 - stated as requirements: +- `R CMD check` and `BiocCheck` pass with no ERROR and no WARNING on current R-devel. This is the + tracker's own wording: "a minimum requirement for package acceptance". It also says "Passing + these checks does not result in automatic acceptance" - a human review follows. +- Run both entry points: `BiocCheck::BiocCheckGitClone()` and + `BiocCheck::BiocCheck('new-package' = TRUE)`. +- Individual files must be <= 5 MB. Upstream states this one as "must". +- `biocViews` present; a vignette and man pages present; maintainer email valid and belonging to + the person submitting; not on CRAN ("a package can only be submitted to one or the other"); + hosted on the GitHub default branch. BiocCheck catches most of these. + +Tier 2 - stated as should or recommended: +- `Version: 0.99.0` for a new package (upstream: "should set"). Expected in practice; set it. +- Source build under 10 MB (upstream: "should occupy less than"). +- `R CMD check --no-build-vignettes` under 10 min (upstream: "should require less than"). +- Vignettes, examples and tests under 8 GB memory (upstream: "it is recommended that"). + +Detail: `knowledge/development/build-check-bioccheck.md` and `knowledge/development/general-dev.md`. + +## Version rule +Start `0.99.0`. Scheme `x.y.z`: `y` odd in devel, even in release (max 99); bump `z` by 1 on +every commit; `0.99.z` becomes `1.0.0` at the first Bioconductor release; `x` changed only by +the Bioconductor team. Detail: `knowledge/maintenance.md`. + +## Bioconductor code style (differs from tidyverse) +Use `<-` for assignment, 4-space indentation, 80-column lines; prefer vectorized code; avoid +`1:n` (use `seq_len`/`seq_along`). Detail: `knowledge/development/r-code.md`. + +## Tooling (use these, do not reimplement them) +This repo ships no validator and no templates on purpose - Bioconductor already maintains both, +and reusing existing infrastructure is itself a review criterion (ch 5). + +```r +# Validation - BiocCheck is authoritative +BiocCheck::BiocCheckGitClone() +BiocCheck::BiocCheck('new-package' = TRUE) + +# Scaffolding - biocthis writes Bioconductor-shaped files +biocthis::use_bioc_description(biocViews = "Software, ") +biocthis::use_bioc_news_md() +biocthis::use_bioc_vignette(name = "", title = "Introduction to ") +biocthis::use_bioc_citation() +biocthis::use_bioc_github_action() +``` + +`use_bioc_description()` writes a **fresh** DESCRIPTION; it does not merge into an existing one. +Internally it calls `usethis::use_description()`, which calls `write_over()`, which asks before +replacing an existing file - and in a non-interactive session it declines silently. So for a +package that already has a DESCRIPTION, this call very often does nothing at all and you get no +error. Add `biocViews` by hand instead, or approve the overwrite knowing it discards the +DESCRIPTION you have. Everything else in the chain appends and is safe on an existing package. + +`biocViews = "Software"` on its own is a BiocCheck **ERROR**: "Add biocViews other than Software". +The top-level terms (Software, AnnotationData, ExperimentData, Workflow) do not count on their +own - pick specific terms from the vocabulary at +https://bioconductor.org/packages/release/BiocViews.html, e.g. +`"Software, GeneExpression, Transcriptomics"`. Two other things BiocCheck flags on a freshly +scaffolded package: the placeholder Description is "too concise" (it wants at least three +sentences), and a Software package with no Bioconductor dependencies gets a warning suggesting +CRAN instead. + +`use_bioc_citation()` leaves `inst/CITATION` unfinished, and unfinished here means broken. The +template substitutes `{{Title}}` and `{{github_owner}}`; the function passes neither a `Title` +nor - on any package that has no GitHub remote configured yet - an owner. The file lands with an +empty title and an empty author, `utils::citation()` errors on either ("a bibentry of bibtype +'Manual' has to specify the field: title"), and because the generated vignette calls `citation()`, +`R CMD build` fails at "creating vignettes". Verified against biocthis 1.23.0 on 2026-08-14. Fill +in the title, the author, and the placeholder `10.1101/TODO` DOI before building anything. + +Install with: + +```r +BiocManager::install(c( + "BiocCheck", "biocthis", + # use_bioc_vignette() adds these to Suggests and refuses to run unless they are installed + "BiocStyle", "knitr", "RefManageR", "sessioninfo", "testthat" +)) +``` + +BiocCheck cannot measure the two +timing gate items (`R CMD check --no-build-vignettes` under 10 min, under 8 GB memory) - those +need a real build. + +Current cycle: Bioconductor release 3.23, devel 3.24, both on R 4.6.0. Build against devel for a +new submission. Never guess this pair - it changes twice a year, `knowledge/SOURCES.md` records +what was verified and when, and https://bioconductor.org/config.yaml is authoritative. + +## Submission and git server (short) +Host on the GitHub default branch, then open an issue (title = package name) at +https://github.com/Bioconductor/Contributions/issues/new (Annotation packages: email +packages@bioconductor.org). The Single Package Builder must pass on all platforms. After +acceptance: register an SSH key at BiocCredentials, add `upstream = git.bioconductor.org`, push +to both remotes; only `devel` and `RELEASE_x_y` branches accept pushes. Full sequence: +`knowledge/workflow.md`. + +For a full submission-readiness audit, use the `bioc-package-review` agent.