boast
boast gathers impact and reach metrics for a piece of research software (and/or its
associated paper) from across code hosts, package registries, and citation/attention
databases, so you can make statements backed by evidence about a tool’s impact — the kind
you make in a grant proposal.
Point it at a Project — identified by any of a code repository, distribution packages, and/or a paper — and it gathers reach Metrics across four Categories (Code, Downloads, Citations, Attention) from a curated set of pluggable Providers, records them in a durable, timestamped Snapshot with full provenance, and renders Reports for grant writing (terminal, Markdown, and an automatically written prose sentence).
Reproducibility and honesty are first principles: dated, attributable Snapshots you can commit and re-render, metrics that are never silently coerced to zero, and totals that never mix incompatible time windows.
boast about samtools/samtools
- Getting started — install boast and run your first Report.
- Concepts — the vocabulary this site and the CLI both use.
- Guides — worked examples for common setups.
- Providers reference and CLI reference — what boast can fetch, and every flag it has.
- Design decisions — the trade-offs behind how boast works.
Getting started
Install
Homebrew (macOS/Linux)
brew install mbhall88/tap/boast
Shell script (macOS/Linux)
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/mbhall88/boast/releases/latest/download/boast-installer.sh | sh
PowerShell (Windows)
powershell -ExecutionPolicy Bypass -c "irm https://github.com/mbhall88/boast/releases/latest/download/boast-installer.ps1 | iex"
conda-forge
conda install -c conda-forge boast
Docker
docker run --rm ghcr.io/mbhall88/boast:latest about 10.1234/journal.xyz
cargo
cargo install boast --locked
From source
git clone https://github.com/mbhall88/boast
cd boast
cargo install --path . --locked
Prebuilt binaries (Linux x86_64/aarch64/armv7 — all statically linked, musl — and macOS x86_64/aarch64, Windows x86_64) are attached to every GitHub Release.
Your first report
Point boast about at anything with a DOI, and it prints a Report straight to your
terminal — no config, no Manifest, no account:
boast about 10.1371/journal.pbio.1002195
Output:
boast 0.1.1 — as of 2026-08-03T05:43:11Z
━━ doi:10.1371/journal.pbio.1002195 ━━
"Big Data: Astronomical or Genomical?" — Zachary D. Stephens et al., PLOS Biology, 2015
── Citations ──
citations 1426 all-time openalex
fwci 59.95 all-time openalex field-weighted citation impact; 1.0 = world average
citation_percentile 99.96 all-time openalex top 1% in its field, year, and type
citations 1166 all-time crossref times referenced, per Crossref
citations 1289 all-time dimensions
recent_citations 165 last two calendar years dimensions resets each 1 January; not a rolling 24-month window
fcr 116.83 all-time dimensions Field Citation Ratio; 1.0 = world average for the field and year
rcr 15.96 all-time dimensions Relative Citation Ratio; 1.0 = NIH-funded benchmark
citations 581 all-time europe_pmc citation count from Europe PMC
── Attention ──
open_access gold all-time openalex OpenAlex open-access status; "closed" means no open-access copy found
wikipedia_mentions 0 all-time wikipedia
altmetric N/A Altmetric attention data not collected: no Details Page API key (ALTMETRIC_KEY)
── Notices ──
This data has been sourced via the Dimensions Metrics API, use of which is subject to the terms at https://dimensions.ai/policies/terms/metrics/. Any use by an unregistered organization is not authorized. Please contact info@dimensions.ai for further information.
English Wikipedia full-text search hits for this DOI; other-language Wikipedias are not counted
Measure a repository
A repository can be measured without a DOI or package. Pass either owner/name or the
full GitHub URL; both forms resolve to the same repository identity:
boast about --repo mbhall88/rasusa
boast about --repo https://github.com/mbhall88/rasusa
Alongside the repository’s Code metrics, the report includes independent indexed-search estimates from OpenAlex and Europe PMC under Attention:
━━ github:mbhall88/rasusa ━━
── Attention ──
mentions 16 all-time openalex
mentions 12 all-time europe_pmc
These are not formal citation counts or verified literal URL occurrences. They are coverage-limited full-text search estimates; self-mentions count, and a preprint and its published version can count separately. The two providers are shown side by side and are never summed. Europe PMC is concentrated in life-sciences literature.
If the piece of software also has a code repository and/or is published on a package registry, tell boast about those too. The repository adds Code and Attention, a package adds Downloads, and a DOI adds paper Citations:
boast about --repo samtools/samtools \
--package conda:bioconda/samtools \
10.1093/gigascience/giab008
Now the Report gains Code and Attention sections for the repo, a Downloads section for the package, and a Downloads Rollup combining the two channels that share a compatible Window — on top of everything the bare DOI already produced above:
━━ github:samtools/samtools ━━
── Code ──
stars 1934 all-time github
forks 613 all-time github
watchers 94 all-time github users watching the repo (subscribers)
repo_age_years 14.40 all-time github since 2012-03-09
contributors 108 all-time github
release_downloads 2156386 all-time github summed across release assets
── Attention ──
mentions 407 all-time openalex
mentions 1104 all-time europe_pmc
━━ conda:bioconda/samtools ━━
── Downloads ──
downloads 9032484 all-time bioconda
═══ Downloads Rollup (derived — see channels above) ═══
11188870 all-time = github:samtools/samtools (2156386) + conda:bioconda/samtools (9032484)
Every run above already wrote a Snapshot — boast about saves one to snapshots/ by
default (pass --no-save to skip that and only print). That first Snapshot is the start
of a history you can diff against later:
boast render snapshots/<the-file-it-just-wrote>.json --format markdown
Output (the raw Markdown source — this is what you’d commit or paste into a report):
# boast Report
_boast 0.1.1 — as of 2026-08-03T05:43:44Z_
## doi:10.1371/journal.pbio.1002195
"Big Data: Astronomical or Genomical?" — Zachary D. Stephens et al., PLOS Biology, 2015
### Citations
| Metric | Value | Window | Provider | Detail |
| --- | --- | --- | --- | --- |
| citations | 1426 | all-time | [openalex](https://api.openalex.org/works/doi:10.1371/journal.pbio.1002195) | |
| fwci | 59.95 | all-time | [openalex](https://api.openalex.org/works/doi:10.1371/journal.pbio.1002195) | field-weighted citation impact; 1.0 = world average |
...
From here, see Concepts for the vocabulary, or jump straight to a Guide that matches your situation.
Concepts
The glossary is shared by boast’s CLI, code, and documentation, so the same terms mean the
same thing everywhere. This page is generated from the canonical copy in the repo,
CONTEXT.md.
Project: The central entity — a single piece of research work that may link to a code repository, one or more distribution packages, and one or more papers. A bare paper lookup (e.g. by DOI) is just a Project whose only linked identity is a paper. Avoid: Tool (too narrow — a Project may be paper-only), Package, Repo
Metric: A single measured quantity of reach for a Project. Every Metric carries: a value, the Provider it came from, the Identity it describes, an as-of timestamp (when it was fetched), and a coverage window (see Window). A raw number with no window and no as-of is not a Metric. Avoid: Stat, statistic, number
Outcome: The result of one Provider×Identity fetch, always exactly one of: Value (a real number), NotApplicable (the Identity legitimately has no presence on that channel — shown as N/A, never 0), or Failed (a transient error: rate limit, timeout, 5xx — the number is obtainable but this attempt didn’t get it). What separates the last two is whether retrying could ever help: a channel boast has no credential to read, or no key configured for, is NotApplicable however it announced itself (ADR-0010). Snapshots record the Outcome explicitly; NotApplicable and Failed are never coerced to 0. Avoid: Status, state, error
Window:
The span of time a Metric’s value covers. Either cumulative (all-time, e.g. crates.io total downloads, GitHub release download_count), trailing (a rolling period, e.g. Homebrew 365-day installs, PyPI last-month), or periodic (a named bucket, e.g. OpenAlex citations in year 2023). Two Metrics may only be summed if their Windows are compatible.
Avoid: Period, timeframe, range
Manifest: An optional file listing one or more Projects, their Identities, any chosen or prioritised Cohort topics, and optional Providers to enable for repeatable or batch runs. Never required: a single Project can be given inline via CLI flags, and a bare paper (DOI/PMID) needs neither. The tool can generate a Manifest from a run, so it is a save-file, not a hand-authored prerequisite. Holds no secrets. Avoid: Config, spec, input file
Snapshot: The primary durable artifact: a timestamped, machine-readable record of every Metric fetched in one run, each with full provenance (Provider, Identity, value, as-of, Window, source URL/response). Snapshots are append-only; a human-readable Report is rendered from a Snapshot, and Snapshots are diffed to show change over time. Avoid: Run, result, output, cache
Report: A human-readable rendering of one or more Snapshots, always derived from Snapshots and never fetching data itself. v1 renderers: a terminal table (default), Markdown (primary saved artifact), and a prose snippet (an automatically written sentence for grant writing). HTML (with over-time charts) and CSV come later. Avoid: Output, summary, document
Notice: A Provider’s licence or terms text, recorded on the Metric it accompanies and shown once per Report in a de-duplicated footer, so attribution travels with the artifact rather than with the terminal session that produced it (ADR-0005). De-duplicated by exact text across every Provider and Identity, because the same boilerplate legitimately repeats on each one. Avoid: Disclaimer, licence blurb, footnote, attribution
Provider Note: An explanation carried by a Provider×Identity Outcome that doesn’t belong on any single Metric: why a fetch yielded no value, failed, or returned only part of the requested data (“no API key configured”, “rate limited after five of twenty Cohort ranks”), or — for a fully successful fetch — provenance detail about how several inputs were aggregated into the reported Metrics (“matched 23 Galaxy CoDex suites: …”). Not a Notice: it describes collection status or provenance rather than the terms behind a Metric, so it appears in its own Report section keyed by Provider and Outcome kind (ADR-0008). Avoid: Error message, warning, notice, detail
Cohort:
The set of repositories a Project’s repo is ranked within, defined by a GitHub topic (e.g. all repos tagged rna-seq, sorted by stars). A Project may be ranked within one or more Cohorts read from the repo’s declared topics, selected explicitly, or prioritised ahead of the remaining declared topics. A Cohort ranking is always reported with its topic named and a disclaimer that GitHub topics are inconsistently applied.
Avoid: Peers, competitors, similar tools, category
Rollup: A derived Metric produced by combining compatible Metrics — e.g. a total downloads value summed across channels. A Rollup must name every Metric it includes and their shared Window; it never silently mixes incompatible Windows or channels. Avoid: Total, sum, aggregate
Category: The family a Metric belongs to, used to group the Report. Five: Code (stars, forks, contributors, release downloads, Cohort rank…), Downloads (per-channel package/install counts + Rollup), Usage (tool executions, server-scoped user accounts, and public-instance availability), Citations (counts + field-normalized FWCI/percentile/FCR/RCR), and Attention (open-access status, Wikipedia mentions, and indexed scholarly repository mentions keyless by default; full news/blog/policy/patent/social breakdown via Altmetric when a key is present). Avoid: Kind, group, type, section
Provider: A component for one source that, given an Identity, fetches zero or more Metrics from one external service (GitHub, Bioconda, OpenAlex, …). Providers are pluggable; the system ships a curated default set and may offer others that a user explicitly enables. Avoid: Source, backend, connector, adapter
Identity: One external handle a Project links to, of a known kind: a code repository, a distribution package (with its registry), or a paper (DOI / PubMed ID). A Provider consumes Identities of the kinds it understands. An Identity always names a piece of work, never a person: a researcher identifier (an ORCID iD) is not an Identity but an input that expands into a set of them (see ADR-0006). Avoid: Handle, reference, link, target
Guides
Worked examples for the shapes a Project usually takes, plus automating the whole thing in CI.
- Measuring a paper — the smallest possible case: a bare DOI or PMID, nothing else.
- A tool with a repo and packages — the common case for research software: a GitHub repo, indexed scholarly mentions, package registries, and a paper.
- A researcher’s whole publication record — expand an ORCID iD into a Manifest covering every work it lists.
- Automating snapshots in CI — a copy-paste GitHub Actions
workflow that runs
boast abouton a schedule and commits the growing history.
Measuring a paper
The smallest possible Project: just a paper, identified by DOI or PubMed ID, with no code repository or package attached. This is the right shape when you’re reporting on a publication itself rather than a specific tool.
Repository scholarly mentions are a separate measurement. Add a repository identity with
--repo when you want OpenAlex and Europe PMC to search for the repository as well; a
DOI-only Project’s Attention rows describe the paper, not its code repository.
boast about 10.1371/journal.pbio.1002195
Output — Citations and Attention Metrics only. Code and Downloads have nothing to attach to without a repository or package, so they’re omitted entirely rather than shown as zero (see ADR-0002; the full transcript, including what a Code/Downloads section looks like once a repo and package are added, is in Getting started):
boast 0.1.1 — as of 2026-08-03T05:43:11Z
━━ doi:10.1371/journal.pbio.1002195 ━━
"Big Data: Astronomical or Genomical?" — Zachary D. Stephens et al., PLOS Biology, 2015
── Citations ──
citations 1426 all-time openalex
...
A bare PubMed ID works the same way:
boast about pmid:26151137
Same paper, different identifier — but not quite the same output. A few Providers (Crossref, Dimensions) only look papers up by DOI, so a PMID-only Project sees fewer of them than a DOI one does:
boast 0.1.1 — as of 2026-08-03T05:46:47Z
━━ pmid:26151137 ━━
── Citations ──
citations 1426 all-time openalex
fwci 59.95 all-time openalex field-weighted citation impact; 1.0 = world average
citation_percentile 99.96 all-time openalex top 1% in its field, year, and type
citations 581 all-time europe_pmc citation count from Europe PMC
Each run already writes a Snapshot to snapshots/ (pass --no-save to only print). See
Diffing the history to compare
one Snapshot against a later run.
A tool with a repository and packages
The common case for research software: a GitHub repository, one or more package registries, and (usually) a paper describing it. Giving boast all three means every Category — Code, Downloads, Citations, Attention — has something to report on.
boast about --repo samtools/samtools \
--package conda:bioconda/samtools \
10.1093/gigascience/giab008
--package is repeatable — list every registry the tool is published on. samtools is
also on Homebrew, so:
boast about --repo samtools/samtools \
--package conda:bioconda/samtools \
--package homebrew:samtools \
10.1093/gigascience/giab008
Count scholarly mentions for a repository
You can query a repository on its own:
boast about --repo mbhall88/rasusa
--repo accepts either owner/name or a full GitHub URL. The repository identity is
normalised before querying, and boast searches the quoted host-qualified phrase
github.com/owner/name in both OpenAlex and Europe PMC. It does not add a bare
owner/name alias, because punctuation-aware matching cannot make that search literal.
The relevant part of the report looks like this:
━━ github:mbhall88/rasusa ━━
── Attention ──
mentions 16 all-time openalex
mentions 12 all-time europe_pmc
These values are indexed full-text search estimates, not formal citation counts or verified literal URL occurrences. Coverage is partial; Europe PMC is concentrated in life-sciences literature; self-mentions count; and article/preprint versions may be counted separately. The providers are independent and their values are never summed.
Output (Code, indexed scholarly mentions under Attention, Downloads per channel, and a Downloads Rollup — Citations are the same shape shown in Getting started, so only the sections that are new here are shown):
━━ github:samtools/samtools ━━
── Code ──
stars 1934 all-time github
forks 613 all-time github
watchers 94 all-time github users watching the repo (subscribers)
repo_age_years 14.40 all-time github since 2012-03-09
contributors 108 all-time github
release_downloads 2156386 all-time github summed across release assets
── Attention ──
mentions 407 all-time openalex
mentions 1104 all-time europe_pmc
━━ conda:bioconda/samtools ━━
── Downloads ──
downloads 9032484 all-time bioconda
━━ homebrew:samtools ━━
── Downloads ──
downloads_30d 503 last 30 days homebrew
downloads_90d 1169 last 90 days homebrew
downloads_365d 5566 last 365 days homebrew
═══ Downloads Rollup (derived — see channels above) ═══
11188870 all-time = github:samtools/samtools (2156386) + conda:bioconda/samtools (9032484)
The Attention section reports independent indexed-search estimates from OpenAlex and Europe PMC for the repository’s host-qualified URL. They are shown side by side and are not summed: each service has different coverage, and the counts can include self-mentions and separate article/preprint versions. The Notice attached to each row explains those limits.
Homebrew’s own Metrics don’t join the Rollup — they’re all trailing Windows (30/90/365 day), and a Rollup can only sum Metrics that share a compatible Window (see Concepts, Rollup and Window); mixing a trailing count in with two all-time counts would misrepresent the total, so it stays out.
Run boast providers to see the full registry of Providers, which Category each
serves, and which package registries they cover.
Container images
Many research tools also ship as a container, on two registries boast covers:
- Docker Hub, as
docker:namespace/name. Official images live underlibrary, soubuntuisdocker:library/ubuntu. - Quay.io, as
quay:namespace/name. This is where Bioconda’s auto-built per-package containers live, so a bioconda recipe gets youquay:biocontainers/<pkg>for free.
If you package for Bioconda, reach for Quay. The biocontainers/ organisation on Docker
Hub is an older, hand-curated set — its samtools image was last pushed in 2019 — while
quay.io/biocontainers is what the build system actually publishes to, and where the
traffic actually goes:
boast about --package quay:biocontainers/samtools \
--package docker:biocontainers/samtools \
--package conda:bioconda/samtools
Captured on a later day than the run above, so bioconda’s count has moved on — which is the point of Snapshots being dated:
━━ quay:biocontainers/samtools ━━
── Downloads ──
pulls 1786502 last 92 days quay
━━ docker:biocontainers/samtools ━━
── Downloads ──
downloads 596337 all-time dockerhub
━━ conda:bioconda/samtools ━━
── Downloads ──
downloads 9055038 all-time bioconda
═══ Downloads Rollup (derived — see channels above) ═══
9651375 all-time = docker:biocontainers/samtools (596337) + conda:bioconda/samtools (9055038)
── Notices ──
Quay.io pull counts record image fetches by machines, not installs by people, and CI re-pulls dominate for a biocontainer; Quay publishes only a rolling daily series, so this is not an all-time total
Docker Hub pull counts record image fetches by machines, not installs by people: CI re-pulls and mirror warming inflate the figure, and it never resets
Note the scale: three months on Quay is triple the lifetime total of the stale Docker Hub image.
The two registries land in the Rollup differently, and it’s the Window that decides — not the fact that both count container pulls:
- Docker Hub publishes an all-time
pull_count, so it shares a cumulative Window with the conda and crates.io counts and joins the Rollup. - Quay publishes only a rolling daily series, never a lifetime total, so its figure
is trailing.
boastreports the window it actually measured —last 92 daysabove, read off the length of the series Quay returned rather than assumed — and a trailing count can’t be summed with all-time ones, so it stays out of the all-time Rollup.
To be precise about the rule: what’s excluded is mixing incompatible Windows, not
trailing Windows as such. Metrics sharing an exactly-equal trailing Window do roll up
together — a Homebrew 30-day install count and a PyPI 30-day download count form their
own last 30 days Rollup, separate from the all-time one. Quay’s ~92-day window simply
has nothing else to pair with today.
Read every container number with its Notice in mind: a pull is a much weaker signal than
an install. Both registries count image fetches by machines, so CI re-runs and mirror
warming land in the same figure, and Docker Hub’s counter never resets —
docker:library/ubuntu sits near ten billion. This is why the Rollup always names each
channel and its own value: the total is only ever as meaningful as the channels you can
see underneath it.
One quirk worth knowing about Quay: it answers a lookup for an image you can’t see with
“requires authentication” rather than “not found”, and it does that identically whether
the image is private or simply doesn’t exist. boast reports that as N/A with a note
saying so, not as a failed fetch — no amount of retrying will turn it into a number, so
it doesn’t make your Snapshot partial.
GitHub’s container registry (ghcr.io) has no equivalent Provider, because GHCR
publishes no pull statistics — neither the OCI registry API nor GitHub’s Packages API
exposes a download count. An image hosted only there can’t contribute to Downloads at
all.
Save the identifiers for next time
Re-typing --repo/--package/the DOI on every run gets old fast. --save writes a
Manifest capturing exactly the identities and any exact or prioritised Cohort selection
used in this run:
boast about --repo samtools/samtools \
--package conda:bioconda/samtools \
--save manifest.toml \
10.1093/gigascience/giab008
From then on:
boast about manifest.toml
You can also build a Manifest up front, without fetching anything, via boast init
(same flags as about):
boast init --repo samtools/samtools --package conda:bioconda/samtools \
--package homebrew:samtools -o manifest.toml 10.1093/gigascience/giab008
manifest.toml now contains, offline, with nothing fetched:
[[project]]
identities = [
"doi:10.1093/gigascience/giab008",
"github:samtools/samtools",
"conda:bioconda/samtools",
"homebrew:samtools",
]
Ranking within a cohort
By default, boast attempts to rank a repository by stars within every GitHub topic it declares. Each rank costs two GitHub Search requests, so a repository with many topics can exhaust Search’s separate per-minute allowance before every rank is collected. The default is best-effort: core GitHub Metrics and completed ranks remain successful, the command exits 0, stderr warns, and the Snapshot carries a durable Provider Note naming every skipped topic. Re-rendering that Snapshot later reproduces the note without touching the network.
Use repeatable --topic for an exact list in command-line order. These topics do not need
to be declared by the repository:
boast about --repo samtools/samtools \
--topic bioinformatics \
--topic sequence-analysis \
10.1093/gigascience/giab008
The Code section gains a cohort_rank row, and a matching disclaimer appears in
Notices:
── Code ──
...
cohort_rank (bioinformatics) 16 all-time github
── Notices ──
#16 of 15275 repos tagged 'bioinformatics'; GitHub topics are inconsistently applied
Use repeatable --priority-topic when every declared topic still matters but some should
consume the available Search allowance first. Priorities are attempted in command-line
order, followed by the remaining declared topics. An undeclared priority is rejected as a
usage error; use --topic when you deliberately want an undeclared Cohort. The exact and
priority modes are mutually exclusive.
boast about --repo samtools/samtools \
--priority-topic bioinformatics \
--priority-topic nextflow
For a run that may wait across GitHub Search quota resets, opt in with
--wait-for-cohort-ranks. The bare flag allows up to five cumulative minutes; set another
positive limit with =, for example --wait-for-cohort-ranks=30s or
--wait-for-cohort-ranks=1h. Boast waits only when GitHub identifies the exhausted resource
as Search and supplies a reset or retry time. It never sleeps on an unrelated 403, malformed
response, or server error. If the cumulative limit expires, the run remains a successful
partial result with a Provider Note.
GITHUB_TOKEN usually raises Search capacity from 10 to 30 requests per minute, but each
Cohort needs two requests and a token therefore does not guarantee all 20 possible topics
fit in one window. Exact selection, priority order, and opt-in waiting remain useful with a
token.
Generated Manifests persist exact selection as topics:
[[project]]
identities = ["github:samtools/samtools"]
topics = ["bioinformatics", "sequence-analysis"]
Priority selection is persisted as priority_topics. The fields are mutually exclusive;
older Manifests containing singular topic = "bioinformatics" remain readable. Command-line
topic options override a Manifest’s selection. Waiting is a runtime choice and is never
stored in the Manifest.
[[project]]
identities = ["github:samtools/samtools"]
priority_topics = ["bioinformatics", "nextflow"]
See the Cohort entry in Concepts for the disclaimer every ranking carries.
A researcher’s whole publication record
Rather than measuring one tool, you can expand a researcher’s ORCID iD into one Project per work in their public record — every paper they’ve published, each as its own bare-paper Project. An ORCID iD identifies a person, never a piece of work, so it isn’t an Identity itself; it’s an input that expands into a set of them (see ADR-0006).
boast init --orcid 0000-0002-1825-0097 -o manifest.toml
(0000-0002-1825-0097 is ORCID’s own public sandbox/demo account — Josiah Carberry, a
fictional professor ORCID maintains specifically for testing against the real API — used
here instead of a real researcher’s iD.)
This performs a real network fetch against the ORCID public API (unlike the rest of
init, which only writes a file from what you already gave it) and writes one
[[project]] entry per work that has a DOI or PMID. Works with neither are skipped —
boast has no Provider that can measure them — and a summary of how many were found,
kept, and skipped is printed to stderr:
WARN orcid:0000-0002-1825-0097: 6 works in record; 6 have a DOI/PMID and will be written to manifest.toml, 0 were skipped (no DOI or PMID — not measurable).
WARN running `boast about` over 6 works ≈ 36 requests across 6 Providers
Manifest written to manifest.toml
manifest.toml (header comment plus the first two of six [[project]] entries):
# Generated by `boast init --orcid 0000-0002-1825-0097` on 2026-08-03 — 6 works.
# 6 written below; 0 skipped (no DOI/PMID).
# Re-run with --include-unidentified to list the skipped works for completion.
# Each remaining work costs ~6 API requests when you run `boast about`.
[[project]]
identities = ["doi:10.5555/12345680"]
[[project]]
identities = ["doi:10.5555/666655554444"]
...
--orcid accepts a bare iD, an orcid:-prefixed one, or a full orcid.org URL, and is
repeatable if you want one Manifest covering several researchers:
boast init --orcid 0000-0002-1825-0097 --orcid 0000-0001-2345-6789 -o manifest.toml
It’s exclusive with every other identity source (positionals, --repo, --package,
--from-file) — an ORCID expansion has no defensible answer to “which of these works
does that repo belong to?”, so mixing them is rejected rather than guessed at.
Works without a DOI or PMID
Most ORCID records carry works boast can’t measure (books, datasets, talks — anything
without a DOI or PMID). By default these are silently dropped. Pass
--include-unidentified to list them instead, as commented-out [[project]] blocks you
can fill in by hand if one of them does have an identifier ORCID just didn’t capture:
boast init --orcid 0000-0002-1825-0097 --include-unidentified -o manifest.toml
The Carberry demo record above happens to have zero unidentified works, so there’s no real run to show for it here — but on a record that has some, each appears (title, year, and work type, straight from the ORCID record) as a commented-out block instead of being silently dropped:
# ─── 1 work with no DOI or PMID in your ORCID record ───
# These can't be measured as-is. If you know the DOI, uncomment the block and
# fill it in — and consider adding it to your ORCID record so it's there next time.
#
# "Some Conference Talk" (2019, lecture-speech)
# [[project]]
# identities = ["doi:"]
Running it
Once you have the Manifest:
boast about manifest.toml
writes one Snapshot per Project. Because that’s usually a lot of Projects for one researcher, the CI automation guide — built for a single Project Manifest — doesn’t directly cover this case; see its “Scope” section for the reason and the workaround.
Snapshot history in CI
A copy-paste GitHub Actions workflow for your own repo (not boast’s) that runs boast about on a schedule, commits the resulting Snapshot, and keeps a rolling Markdown Report
up to date — so boast diff has real history to compare against with zero manual work.
This is deliberately YAML you copy and edit, not a published Action or reusable workflow. Everyone adopting it customises something (the Manifest path, the schedule, which identities it covers), and a template invites that where a versioned Action would fight it. When a scheduled run fails at 3am it’s almost always a rate limit or a network blip — plain YAML has no indirection to dig through to find out why.
Prerequisites
A Manifest listing the single Project you want to track (see “Scope” below), committed to the repo. Build one once with:
boast init --repo owner/name --package crates:name 10.1234/journal.xyz
Commit the resulting manifest.toml. (boast init --orcid <ORCID iD> builds a Manifest too,
but typically lists many Projects — one per publication — which this template’s report step
doesn’t cover; see “Scope” below before using it here.)
The workflow
Save as .github/workflows/impact-snapshot.yml:
name: Impact snapshot
on:
schedule:
# 03:00 UTC on the 1st of every month. Citations, downloads, and stars
# move slowly — weekly mostly adds diff noise and repo churn for near-
# identical numbers. Monthly gives a clean twelve-points-a-year series,
# matching how these figures actually get quoted ("as of March 2026").
- cron: '0 3 1 * *'
workflow_dispatch: {} # lets you trigger a run by hand to test the workflow
permissions:
contents: write # needed to commit and push the Snapshot + report
jobs:
snapshot:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install boast
run: |
set -euo pipefail
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/mbhall88/boast/releases/latest/download/boast-installer.sh | sh
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
# Deliberately NOT `continue-on-error` and NOT `|| true`: `boast about`
# exits 1 if any Provider fetch failed (rate limit, timeout, ...), and
# that should still turn this job red so a real, persistent problem
# doesn't go unnoticed. What it must NOT do is stop the Snapshot from
# being written or committed — a partial Snapshot is honest data (a
# `Failed` Outcome is recorded, never dropped), and skipping the commit
# would additionally punch a silent gap in the history that a later
# `diff` couldn't explain.
- name: Run boast about
# Scheduled jobs can afford to wait for a confirmed GitHub Search
# reset; the five-minute cumulative cap prevents an unbounded job.
run: boast about manifest.toml --snapshot-dir snapshots --wait-for-cohort-ranks=5m
env:
# Auto-provided by Actions — raises GitHub's API rate limit for the
# Code category. No repo secret needed for this one.
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Optional: only set this repo secret if you have an Altmetric
# Details Page API key. Without it, Altmetric metrics are reported
# as not-applicable rather than fetched — everything else still runs.
ALTMETRIC_KEY: ${{ secrets.ALTMETRIC_KEY }}
# `if: always()` so this and the commit step below still run even when
# the step above exited non-zero. `boast render` itself also exits 1
# for a Snapshot recording a `Failed` Outcome, so on a partial failure
# this step goes red too, alongside `boast about` above — expected,
# not a second problem: IMPACT.md is still written correctly (the
# FAILED row included, per ADR-0002) and still gets committed below.
- name: Regenerate the rolling report
if: always()
run: |
set -euo pipefail
newest=$(find snapshots -maxdepth 1 -name '*.json' | sort | tail -n1)
boast render "$newest" --format markdown > IMPACT.md
- name: Commit snapshot and report
if: always()
run: |
set -euo pipefail
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add snapshots IMPACT.md
git diff --cached --quiet && exit 0
git commit -m "chore: monthly impact snapshot"
git push
The knobs
- Schedule — the
cron:line.'0 3 1 * *'is monthly; tighten it (e.g.'0 3 * * 1'for weekly on Mondays) if your numbers move fast enough to be worth the extradiffnoise and repo churn. - Manifest path —
manifest.tomlin theboast aboutstep. Point it at wherever your Manifest lives if it isn’t at the repo root. - Snapshot directory —
--snapshot-dir snapshots(boast’s own default). Snapshots are named by boast itself from the run timestamp (YYYYMMDDTHHMMSSZ.jsonwhen driven by bare identifiers) — already unique, already lexically sortable, already carrying the as-of time. When a Manifest drives the run, boast also suffixes the filename with the Project’s own identity (e.g.20260301T030001Z-doi-10.1234-journal.xyz.json) so that multiple Projects sharing one Manifest never collide — still lexically sortable, just not a bare timestamp. - Report filename —
IMPACT.md, overwritten every run rather than timestamped. It’s offline and deterministic (boast rendernever touches the network — see ADR-0001), so regenerating it is nearly free, and a stable filename gives you one current page to link to from your README instead of hundreds of near-identical Markdown files accumulating over the years.
Make sure snapshots/ (and IMPACT.md) aren’t excluded by your repo’s .gitignore — it’s
an easy thing to have picked up from a template that assumed the opposite.
Why commit Snapshots instead of uploading them as workflow artifacts? Artifacts expire
(90-day retention by default) and would silently evaporate the accumulating history that’s
the entire point of this workflow — and you can’t diff two of them without downloading
both by hand first. A committed file has neither problem.
One Project per Manifest
This template’s “Regenerate the rolling report” step renders whichever Snapshot file sorts
last — correct as long as your Manifest lists a single [[project]] (the common case:
tracking your own tool’s reach, which is why the Prerequisites example above builds a
one Project Manifest). A Manifest listing several Projects makes boast about manifest.toml
write one Snapshot file per Project on every run; the “newest” pick above then only covers
whichever Project’s file happens to sort last, silently leaving the others out of IMPACT.md.
If you need one report covering several Projects, render each Project’s own newest Snapshot
into its own file (e.g. loop over the distinct filename suffixes) rather than trying to
squeeze them into one IMPACT.md, or run this workflow once per Project against separate
single Project Manifests.
Diffing the history once you have it
Once you’ve got two or more committed Snapshots, compare any pair directly (filenames carry the Project’s own identity suffix, per “The knobs” above):
boast diff snapshots/20260301T030001Z-doi-10.1234-journal.xyz.json \
snapshots/20260401T030001Z-doi-10.1234-journal.xyz.json
Keys as repo secrets
GITHUB_TOKEN— the workflow above uses the token Actions injects automatically (secrets.GITHUB_TOKEN); you don’t need to create anything. It raises GitHub’s core limit and usually raises the separate Search limit from 10 to 30 requests per minute. A Cohort rank costs two Search requests, so a token does not guarantee that all 20 possible topics fit in one window; the workflow’s bounded wait can continue across a confirmed reset.ALTMETRIC_KEY— optional, and only relevant if you have an Altmetric Details Page API key (not an Explorer key — they’re different products with different credentials). Add it under Settings → Secrets and variables → Actions → New repository secret on your repo. Without it, Attention category Altmetric metrics report as not-applicable; every other Provider is unaffected.
Run boast providers to see the full, current list of which Providers need which key.
Galaxy tool-execution usage
Galaxy is a web platform for running bioinformatics tools without writing code. Many command-line tools also ship as a Galaxy wrapper, and the Galaxy Tool CoDex project publishes cross-server execution and user-account totals for every wrapper it knows about — real evidence of use beyond stars and downloads, if your tool has one.
Galaxy is optional and off by default. Most Projects have no Galaxy wrapper at all,
so fetching it on every run would add a mostly-empty request rather than a useful one.
Check boast providers to see it listed under Usage with DEFAULT set to no.
Enabling it
boast about --repo mbhall88/rasusa --enable-provider galaxy
--enable-provider is repeatable and generic — it isn’t Galaxy-specific — so a future
optional Provider uses the same flag. An unknown name is a usage error:
$ boast about --repo owner/name --enable-provider not-a-real-provider
error: unknown optional provider 'not-a-real-provider' (available: galaxy)
Galaxy only understands repository Identities (--repo, a GitHub URL, or the bare
owner/name positional); it has no package- or paper-level presence to report on.
What each Metric means
━━ github:mbhall88/rasusa ━━
── Usage ──
runs 104 all-time galaxy
user_accounts 24 all-time galaxy
public_instances 2 all-time galaxy available on UseGalaxy.eu, UseGalaxy.fr
── Notices ──
Galaxy CoDex republishes cumulative tool-execution counts on its own refresh schedule, so this figure may lag the underlying Galaxy servers
Runs: aggregated sum. Users: max unique per wrapper (to prevent over-counting) — not a count of distinct people, since CoDex's per-server totals can count one account once per server. Both are cumulative CoDex figures that may lag the underlying Galaxy servers.
runs and user_accounts each carry a caveat long enough to be shown once in the
Notices footer rather than repeated inline on every row — see
Concepts for how boast decides between the two.
runs— cumulative tool executions across every Galaxy wrapper CoDex has matched to this repository, summed across CoDex’s tracked servers.user_accounts— the maximum per-wrapper account count, not a sum (see below). This is not a count of distinct people: CoDex’s own figures can count one account separately per server, so treat it as “at least this many accounts,” never as a headcount.public_instances— how many of the four major public Galaxy servers (UseGalaxy.org, UseGalaxy.org.au, UseGalaxy.eu, UseGalaxy.fr) have the tool installed, regardless of whether it’s actually been run there. The Metric’s note names which ones matched.
A repository with no matching CoDex entry is NotApplicable, never a real zero — most
repositories simply have no Galaxy wrapper.
Multi-suite aggregation
One upstream tool can have several Galaxy wrappers, one per subcommand — CoDex calls
each a “suite.” vcflib alone maps to 23. When more than one suite matches a
repository:
runsis summed across every matched suite — it’s a genuine execution count, and summing doesn’t over-count anything.user_accountsis not summed — the maximum across the matched suites is used instead, since the same account can run more than one subcommand of the same tool. Summing would inflate “how many accounts used this tool” into “how many (account, subcommand) pairs were used.”public_instancesis a union — available if any matched suite is installed there.
Every matched suite ID is recorded in a Provider Note so the aggregation is auditable:
── Provider Notes ──
galaxy: Matched 23 Galaxy CoDex suites: vcf2tsv, vcfaddinfo, vcfallelicprimitives, ... — github:vcflib/vcflib
Matching is exact, on CoDex’s Homepage field only (normalised the same way boast
normalises any GitHub URL) — never on CoDex’s Suite source, which names the wrapper
repository (typically a shared multi-tool repo like galaxyproject/tools-iuc), not the
upstream Project actually being measured.
Why the numbers may lag
CoDex republishes its dataset on its own refresh schedule, independent of the live
Galaxy servers — runs and user_accounts both carry a note saying so, since they’re
cumulative counts CoDex derives from its own snapshot. Treat these figures as “at least
this much use, as of whenever CoDex last refreshed,” not a live server query.
public_instances doesn’t carry the same caveat: installation is a current-state fact,
not a count that accumulates staleness the same way. See
ADR-0011 for
why CoDex was chosen over the live Galaxy Europe dashboard and ToolShed’s install counts.
Persisting the choice in a Manifest
--save/boast init persist an explicit --enable-provider into the Manifest’s
enable_providers, so a scheduled run doesn’t need to repeat the flag:
[[project]]
identities = ["github:mbhall88/rasusa"]
enable_providers = ["galaxy"]
boast about manifest.toml then fetches Galaxy for that Project automatically. An
explicit --enable-provider on the command line overrides a Manifest’s own selection
entirely for that run, the same override rule --topic already applies to Cohort
selection.
Providers reference
Every Provider in boast’s default registry, which Category it serves, whether it’s
enabled by default, and what environment variable (if any) it needs a key in. This page
is generated from boast providers — the same command you can run yourself to check
what’s obtainable before running about.
PROVIDER CATEGORY DEFAULT KEY
github Code yes optional: GITHUB_TOKEN (not set)
crates.io Downloads yes none
anaconda Downloads yes none
pypi Downloads yes none
homebrew Downloads yes none
dockerhub Downloads yes none
quay Downloads yes none
galaxy Usage no none
openalex Citations yes none
crossref Citations yes none
dimensions Citations yes none
europe_pmc Citations yes none
wikipedia Attention yes none
altmetric Attention yes required: ALTMETRIC_KEY (not set)
An optional key raises a rate limit or unlocks extra Metrics but isn’t required; a required key means that Provider reports every Metric as not-applicable until it’s set (never as zero — see ADR-0002).
OpenAlex and Europe PMC also accept GitHub repository identities. For a repo they add an
independent mentions Metric under Attention, based on each service’s indexed full-text
search. These are coverage-limited estimates, not formal citation counts or verified
literal URL occurrences; the values are shown side by side and are never summed.
CLI reference
Every subcommand and flag, straight from --help.
boast
Gather reach and impact metrics for a research tool or paper into dated, quotable snapshots.
Usage: boast [OPTIONS] <COMMAND>
Commands:
about Fetch metrics for a Project, write a Snapshot, and print a report. A single `.toml` positional (see `boast init`) is loaded as a Manifest instead, running every Project it lists
render Render a stored Snapshot as Markdown or prose. Never touches the network (ADR-0001) — offline and deterministic for a given Snapshot
diff Compare two stored Snapshots and report the change in each shared Metric. Never touches the network (ADR-0001)
providers List the registered Providers: Category, default-enabled status, and key requirement. Never touches the network
init Write a Manifest TOML file from identifiers, without fetching — unless `--orcid` expands a researcher's record, which does (see its own help)
help Print this message or the help of the given subcommand(s)
Options:
-v, --verbose... Increase logging verbosity (-v info, -vv debug, -vvv trace)
-q, --quiet Silence all logging except errors
-h, --help Print help
-V, --version Print version
boast about
Fetch metrics for a Project, write a Snapshot, and print a report. A single `.toml` positional (see `boast init`) is loaded as a Manifest instead, running every Project it lists
Usage: boast about [OPTIONS] [IDENTIFIER]...
Arguments:
[IDENTIFIER]... Identifiers: a DOI, doi.org URL, `pmid:12345678`, a github.com URL, `owner/name`, or a package as `registry:name` (e.g. `crates:boast`)
Options:
-r, --repo <OWNER/NAME>
A GitHub repository as `owner/name` (alternative to a positional; repeatable)
-p, --package <REGISTRY:NAME>
A distribution package as `registry:name`, e.g. `crates:boast` (alternative to a positional; repeatable)
-f, --from-file <FILE>
Read identifiers from a file (one per line; `#` comments and blank lines ignored). Use `-` for stdin. Repeatable
-t, --topic <TOPIC>
GitHub Cohorts to rank explicitly, in argument order. Repeatable and exclusive with --priority-topic; overrides Manifest selection
--priority-topic <TOPIC>
Declared GitHub Cohorts to rank first, in argument order, before every remaining declared topic. Repeatable and exclusive with --topic
-v, --verbose...
Increase logging verbosity (-v info, -vv debug, -vvv trace)
--wait-for-cohort-ranks[=<DURATION>]
Wait across confirmed GitHub Search quota resets for Cohort ranks. A bare flag allows five cumulative minutes; custom values require `=`. Without this flag, quota exhaustion returns successful partial results with skipped topics in a durable Provider Note. GITHUB_TOKEN raises usual Search capacity but cannot guarantee all Cohorts fit
-d, --snapshot-dir <DIR>
Directory to write the Snapshot into [default: snapshots]
-q, --quiet
Silence all logging except errors
-n, --no-save
Print the report but do not write a Snapshot file
-s, --save <FILE>
After fetching, also write a Manifest reflecting the identities and Cohort selection used in this run, so a future run can `boast about <file>` instead of re-typing them. Not available when the input is itself a Manifest — use `boast init` to build one up front instead
-e, --enable-provider <NAME>
Enable an optional Provider by name (repeatable), e.g. `galaxy`. Off by default (see `boast providers`' DEFAULT column) — an unknown name is a usage error. Overrides, rather than adds to, a Manifest input's own `enable_providers` for every Project it runs
-j, --threads <N>
Maximum number of distinct hosts fetched from concurrently. Never more than one request is in flight against the *same* host no matter how high this is set (ADR-0007). Raising it past the number of hosts a Project actually touches (at most the Provider registry's size, ~13 by default) buys nothing; lower it to open fewer simultaneous connections [default: 8]
-h, --help
Print help
boast render
Render a stored Snapshot as Markdown or prose. Never touches the network (ADR-0001) — offline and deterministic for a given Snapshot
Usage: boast render [OPTIONS] <SNAPSHOT>
Arguments:
<SNAPSHOT>
Path to a Snapshot JSON file written by `boast about`
Options:
-f, --format <FORMAT>
Output format
Possible values:
- markdown: Markdown Report grouped by Category — the primary saved artifact
- prose: A single sentence for grant writing, summarising the headline Metrics
[default: markdown]
-v, --verbose...
Increase logging verbosity (-v info, -vv debug, -vvv trace)
-q, --quiet
Silence all logging except errors
-h, --help
Print help (see a summary with '-h')
boast diff
Compare two stored Snapshots and report the change in each shared Metric. Never touches the network (ADR-0001)
Usage: boast diff [OPTIONS] <OLD> <NEW>
Arguments:
<OLD> The earlier Snapshot JSON file
<NEW> The later Snapshot JSON file
Options:
-v, --verbose... Increase logging verbosity (-v info, -vv debug, -vvv trace)
-q, --quiet Silence all logging except errors
-h, --help Print help
boast providers
List the registered Providers: Category, default-enabled status, and key requirement. Never touches the network
Usage: boast providers [OPTIONS]
Options:
-v, --verbose... Increase logging verbosity (-v info, -vv debug, -vvv trace)
-q, --quiet Silence all logging except errors
-h, --help Print help
boast init
Write a Manifest TOML file from identifiers, without fetching — unless `--orcid` expands a researcher's record, which does (see its own help)
Usage: boast init [OPTIONS] [IDENTIFIER]...
Arguments:
[IDENTIFIER]... Identifiers: a DOI, doi.org URL, `pmid:12345678`, a github.com URL, `owner/name`, or a package as `registry:name` (e.g. `crates:boast`)
Options:
-r, --repo <OWNER/NAME> A GitHub repository as `owner/name` (alternative to a positional; repeatable)
-p, --package <REGISTRY:NAME> A distribution package as `registry:name`, e.g. `crates:boast` (alternative to a positional; repeatable)
-f, --from-file <FILE> Read identifiers from a file (one per line; `#` comments and blank lines ignored). Use `-` for stdin. Repeatable
-t, --topic <TOPIC> Exact GitHub Cohorts to record in the Manifest, in argument order
--priority-topic <TOPIC> Declared GitHub Cohorts to prioritise in the Manifest, in argument order
-e, --enable-provider <NAME> Optional Providers to record in the Manifest's `enable_providers` (repeatable), e.g. `galaxy`. An unknown name is a usage error
-v, --verbose... Increase logging verbosity (-v info, -vv debug, -vvv trace)
-o, --output <FILE> Where to write the Manifest [default: manifest.toml]
-q, --quiet Silence all logging except errors
-O, --orcid <ORCID> Expand a researcher's ORCID iD (bare, `orcid:`-prefixed, or an orcid.org URL) into a Manifest of every work with a DOI or PMID, one Project per work (ADR-0006; repeatable). **Performs a network fetch** — unlike the rest of `init`, which is otherwise offline. Exclusive with positionals/`--repo`/`--package`/`--from-file`: an ORCID expansion has no defensible answer to "which of these works does that repo belong to?"
-u, --include-unidentified With `--orcid`, also list works with neither a DOI nor a PMID (and so were skipped) as commented-out `[[project]]` blocks you can fill in by hand. Off by default: most ORCID records carry many such works
-h, --help Print help
Design decisions
boast’s architecture decision records (ADRs), unabridged and in order. Each one records a trade-off with alternatives, written down while the reasoning was fresh so it doesn’t have to be reconstructed later from code archaeology.
The canonical copies live in docs/adr/
in the repo; the pages here are generated from those files, not duplicated by hand.
For the original end-to-end design — the v1 spec, before it was broken up into ADRs —
see docs/spec/0001-boast-v1.md.
Architecture centred on Snapshots
Status
accepted
Context and decision
boast produces numbers that people quote in grant proposals (“as of March 2026, 2,400 citations”). Those claims must stay defensible after the underlying metrics move on. We therefore make a durable, append-only Snapshot — not a printed report — the primary artifact of a run.
boast about fetches live and writes a timestamped, machine-readable Snapshot recording every Metric with full provenance (Provider, Identity, value, as-of timestamp, Window, source). A Report (terminal table, Markdown, prose, HTML, CSV) is always rendered from a Snapshot and never fetches data itself. render and diff operate purely offline on stored Snapshots; growth-over-time comes from diffing Snapshots, so no database is needed — just committable files.
Considered options
- Stateless print-and-forget. Simplest, but nothing is reproducible after the fact: re-running next month silently yields different numbers with no record of what was originally quoted. Rejected — reproducibility is the whole point.
- A database of metrics over time. More power (queries, dashboards) but heavy operational surface for a personal, shareable CLI. Rejected for v1 in favour of append-only JSON files that live next to the grant draft in git.
Consequences
- Snapshots are internally consistent as-of a single moment. There is deliberately no “refresh one failed Provider into an existing Snapshot” — a re-run produces a new Snapshot rather than a patchwork of fetch times.
aboutis always-live (no cross-run cache that could serve stale numbers);render/diffare always-offline. The two verbs mean “get the truth now” vs “work with truths already captured.”- The Snapshot is the compatibility surface: it carries a versioned schema so old Snapshots remain renderable as the tool evolves.
Metric honesty model
Status
accepted — rule 2’s channel-comparability clause is refined by ADR-0009, which lets a weaker-unit channel join a Rollup provided its caveat travels with the total into every format; rule 1’s “missing key” example is refined by ADR-0010, which puts a permanently unreadable channel under NotApplicable rather than Failed
Context and decision
boast exists to make impact claims that survive scrutiny, so the data model is built to make understating impossible to do by accident and overstating impossible to do silently. Three rules govern the model:
-
Three-state Outcome. Every Provider×Identity fetch resolves to exactly one of
Value(a real number),NotApplicable(the Identity legitimately has no presence on that channel — e.g. samtools has no npm package), orFailed(a transient error: rate limit, timeout, 5xx).NotApplicableandFailedare never coerced to 0 — a missing number and a zero number are different facts, and conflating them silently understates a tool’s reach. The dividing line is whether a number is obtainable: a channel no retry and no available configuration could ever yield — a missing key, or an auth wall boast has no credential for — isNotApplicable, notFailed. See ADR-0010, which replaces this clause’s original “missing key” example. -
Windows gate summation. Every Metric carries a coverage Window —
cumulative(all-time),trailing(rolling N days, e.g. Homebrew’s 365-day installs), orperiodic(a named bucket). Metrics may only be combined into a Rollup when their Windows are compatible, and a Rollup must name every Metric it includes. The tool never silently sums an all-time crates.io count with a 365-day Homebrew count. Channels whose units differ in strength (a Conda install ≠ a Docker pull ≠ a git clone) may still be summed, but only where the Rollup names each one and the weaker channel’s caveat travels with the total into every format it appears in — see ADR-0009, which replaces this clause’s original blanket prohibition. -
Best-effort with a truthful exit code. One dead Provider never blocks the rest; transient failures get retries with backoff; but the process exits non-zero if any
Failedoutcomes remain, so a partial Snapshot is distinguishable from a complete one before anyone quotes it. Reports visibly mark partial data.
Considered options
- Coerce missing/failed to 0 and always exit 0. Simpler code and prettier tables, but it turns the tool into something that quietly lies in the exact direction that damages a grant. Rejected outright.
- Fail-fast on the first Provider error. One rate-limited API would abort an otherwise-complete run. Rejected in favour of best-effort + explicit per-fetch Outcomes.
Consequences
- The Snapshot schema must represent
NotApplicable/Failedexplicitly (with the error), not by omission. - Callers (CI, scripts, the user) can gate on exit code to avoid quoting incomplete data.
Data source strategy and deliberate exclusions
Status
accepted
Context
Impact data comes from many services with different coverage, cost, terms, and stability. That set changed materially in late 2025 and early 2026. This ADR records why the default Provider set looks the way it does, including the deliberate exclusions, so nobody re-litigates them in six months.
Decision
Default set (all keyless unless noted), grouped by Category:
- Code: GitHub (stars, forks, watchers, contributors, total release downloads, repo age, Cohort rank by topic).
- Downloads: Anaconda.org (any channel — bioconda, conda-forge, or otherwise), PyPI, GitHub release assets, crates.io, Homebrew — reported per-channel, with a labelled Rollup.
- Citations: OpenAlex (headline count + field-normalized FWCI +
citation_normalized_percentile), Crossref (authoritative metadata), Europe PMC (life sciences cross-check), Dimensions badge API (count + recent count from the last two calendar years + FCR + RCR). - Attention: open-access status (OpenAlex), Wikipedia mentions, and indexed scholarly repository mentions (OpenAlex + Europe PMC) as keyless baseline defaults; Altmetric as an opt-in richer Provider.
Ranking. A paper’s “standing among similar work” is delivered by field-normalized metrics that already exist for free — OpenAlex percentile/FWCI and Dimensions FCR/RCR — rather than any manually constructed ranking. A repo’s peer comparison is a GitHub topic Cohort (rank by stars among repos carrying a topic), chosen because it needs no manual setup and is reproducible; its dependence on inconsistent topic tagging is disclosed in every Report, not hidden.
Deliberate exclusions (the less obvious part)
- No Google Scholar. No official API, robots.txt forbids automated access, and it blocks scrapers aggressively; reliable access needs a paid third-party proxy. A built-in scraper would be the one component that silently breaks and can get a user’s IP blocked, which is fatal for a tool that must be reliable and reproducible. Excluded despite it being a metric users personally like.
- Altmetric is key-gated, not default-free. As of 10 November 2025 Altmetric’s Details-Page API requires an API key for all users; the old free badge endpoint now 403s. Rich attention data requires a key. That is a property of Altmetric’s service, not a limitation of
boast.ALTMETRIC_KEYmust specifically be a Details Page API key (the/v1/fetch/doi/{doi}lookup for one article that this Provider calls) — Altmetric Explorer, the institutional analytics dashboard product, is a different API with its own key/secret pair that will not authenticate here (confirmed directly against the live API: an Explorer credential gets a clear “API key … not recognized”). A Details Page API key comes from either an institutional licence (ask your library — many universities that pay for Explorer don’t automatically also license the Details Page API for individual researchers) or Altmetric’s SRAD (Scientometric Research Access to Data) program, a free application-based route for non-commercial research. Neither is instant, so don’t expect a same-day key. The exact field-name shape this Provider parses (score,cited_by_msm_count,cited_by_feeds_count,cited_by_policies_count,cited_by_patents_count,cited_by_tweeters_count,readers.mendeley) was cross-checked against public documentation and a real third-party client’s source, but has never been confirmed against a live successful response — nobody involved in building this had Details Page API access. If you get real access, running the Provider once and comparing output against what you see on the paper’s own Altmetric page is the one remaining gap; a response this Provider can’t recognise at all comes backFailed, not a silent zero, specifically so a schema mismatch can’t masquerade as “no attention.” - No Crossref Event Data. The main free, keyless attention feed (Wikipedia/news/blog/social mentions of a DOI) was sunset on 23 April 2026; its replacement only exposes dataset-citation relationships. This is why keyless attention is “lite” (OA status + Wikipedia) rather than a full attention donut.
- GitHub “used by / dependents” is opt-in, not default. Arguably the best reach signal for a library, but there is no API — the count only exists on the scraped
/network/dependentsHTML page and breaks when GitHub changes markup. Offered as an optional metric with an explicit caveat rather than a default. - Issue/PR counts omitted from the default Code set — they read as activity/maintenance, not reach.
Consequences
- Providers are pluggable behind a common trait so paid/optional sources (Semantic Scholar, Altmetric, GitLab, Docker/Quay, CRAN, Bioconductor, npm) and future replacements slot in without touching the core.
- Reports must carry source attribution and the topic Cohort disclaimer, because the credibility of a claim depends on which Provider produced it.
No host-native dependencies; rustls, not OpenSSL
Status
accepted
Context and decision
boast is meant to be trivially installable and distributable everywhere (crates.io, Bioconda, Homebrew) and to cross-compile cleanly, including to static *-musl targets. The classic blocker to that is a dependency that links a host C library — above all OpenSSL, which routinely breaks cross-compilation and static linking.
We therefore adopt a dependency policy: no crate that requires a host-installed native library. Concretely:
- TLS is rustls, never
openssl/native-tls. The HTTP stack must be built with rustls (e.g.reqwestwithdefault-features = false+rustls-tls, or a rustls-based client such asureq).native-tls/openssl/openssl-sysare prohibited anywhere in the tree. - Prefer pure-Rust crates and avoid
*-syscrates. Parsing, serialisation (serde/JSON/TOML), and everything else should be pure Rust so that a cross-build needs only a Rust toolchain and the target, not a cross C toolchain or vendored system libraries. - CI builds and releases static
x86_64-unknown-linux-musl(and other) targets to prove the constraint holds and to ship dependency-free binaries.
Considered options
reqwestwith the defaultnative-tls/OpenSSL backend — the most common Rust HTTP setup, but it drags in OpenSSL and the cross-compilation/static-linking pain we are explicitly avoiding. Rejected.- rustls with
aws-lc-rsvsringbackend — both avoid host OpenSSL and cross-compile far more cleanly than OpenSSL; either is acceptable. If a build environment makesaws-lc-rs’s C/asm awkward, theringbackend is the fallback. This is an internal knob, not a user-facing decision.
Consequences
- The concrete HTTP client is contained behind the single HTTP-transport seam (see the v1 spec and ADR-0001/0002), so swapping clients — or TLS backends — is a localised change that does not touch Providers or the Snapshot model.
- Any future Provider or feature that would pull in a host-native dependency must be reworked or rejected; this constraint outranks convenience.
- rustls validates against a bundled/vendored root store (e.g.
webpki-roots) rather than the host trust store, keeping behaviour identical across platforms — an intended consequence, not an oversight.
Provider licence notices belong in Reports, not logs
Status
accepted — refined by ADR-0008, which scopes this footer to licence/terms text and gives NotApplicable/Failed messages a sibling section of their own
Context and decision
Some Providers’ terms require visible attribution wherever their data is displayed — e.g. the Dimensions Metrics API terms ask for “an attribution [on] the page where the metrics are displayed” (added alongside the Dimensions Provider, #8). A boast Report is rendered from a Snapshot and can be rendered long after the original fetch: render/diff work purely offline on stored Snapshot JSON with no re-fetch (ADR-0001). A notice printed only to stderr during the original about run would not exist by the time someone later runs render on the saved Snapshot, hands the Snapshot file to a teammate, or pastes a rendered Report into a grant draft. In all of those cases, the Report is the page where the metrics are displayed.
So a Provider’s licence/terms notice is recorded on the Metric it accompanies (Metric.note), inside the Snapshot, so it survives serialization and offline re-rendering — and it is shown in the Report, not logged. To avoid repeating the same notice once per Identity (e.g. several DOIs all carrying the same Dimensions boilerplate), the terminal Report treats any note past a length threshold as a Provider-level notice rather than a per-row interpretive gloss, de-duplicates by exact text, and prints each distinct notice once in a footer section — so a run over many DOIs still shows the notice exactly once, not once per DOI.
Considered options
- Log to stderr instead of the Report. Keeps the table clean and matches how these terms are usually satisfied in practice (a webpage embedding a Provider’s JS badge, not a CLI). Rejected: a Snapshot is meant to be rendered again later without re-running the fetch (ADR-0001), and a log line from the original run wouldn’t be there for that later render or for a teammate handed just the Snapshot file.
- Print inline on every Metric/row that carries the notice. Simplest, but produces a duplicated, table-breaking wall of text as soon as more than one Identity shares the Provider (a ~250-character notice on every DOI’s
citationsrow). Rejected in favour of a de-duplicated, once-per-run footer.
Consequences
Metric.notedoes double duty for v1 — a short interpretive gloss (e.g. “field-weighted citation impact; 1.0 = world average”) or a Provider’s legal notice — distinguished purely by length in the renderer. A future Provider needing the same treatment reuses this without a Snapshot schema change; if the distinction ever needs to be explicit, promoting it to a typed field is the escape hatch.- Reports must carry a duplicate-free but complete set of the notices behind the numbers they display — the Report is the artifact people paste into a grant proposal, so attribution travels with it, not with the terminal session that produced it.
ORCID expands to Identities; it is not one
Status
accepted
Context and decision
An ORCID iD identifies a researcher. Every existing Identity — a paper, a code repository, a distribution package — identifies a piece of work that a Provider can fetch Metrics for. Supporting ORCID therefore forced a choice about which of those two things it is.
We decided: an ORCID is an input expander, not an Identity. boast init --orcid <ORCID> resolves it, once, into the set of Paper Identities the researcher has claimed, and writes them to a Manifest. The Identity enum is unchanged, and no Provider ever receives an ORCID.
Two consequences follow directly, and are deliberate rather than incidental:
boast about orcid:…is refused, with an error pointing atboast init --orcid.Identity::parserecognises the ORCID shape (bare,orcid:-prefixed, and URL forms) solely in order to give a better refusal than the generic “could not recognise” catch-all.- Expansion produces one Project per work.
CONTEXT.mddefines a Project as “a single piece of research work”, so a researcher’s 118 papers are 118 Projects, not one Project with 118 papers. This reuses the existing Manifest batch pipeline whole, with no new orchestration.
The expansion reads ORCID’s own public API (pub.orcid.org/v3.0/{orcid}/works), which is keyless and returns the researcher’s self-curated record.
Considered options
-
Make ORCID a new Identity kind (
Identity::Researcher), with a Provider emitting Metrics about the researcher — h-index, i10-index, works count, total citations. These are available without a key: OpenAlex’s/authors/{orcid}endpoint returns all of them in one call, and h-index is relevant to the grant-writing use case boast exists to serve. Rejected for now, on product grounds rather than technical ones: it would require rewritingCONTEXT.md’s definitions of both Project (“a single piece of research work”) and Identity (“one external handle a Project links to”), because a researcher is neither. That shifts boast from “how far did this piece of work reach” toward “how accomplished is this person” — a decision that deserves to be made deliberately, not to arrive as a side effect of adding ORCID support. Tracked separately; this ADR would need superseding if it ships. -
Expand via OpenAlex’s author→works path (
/authors/{orcid}→works_api_url) instead of ORCID’s own API. It finds work the researcher never claimed, but attribution is algorithmic and produces false positives on common names. That would spend requests measuring other people’s papers and report the total as yours. Rejected: “what I claim as mine” is the better source of truth than “what an algorithm infers is mine”, and a thin ORCID record is best fixed at ORCID, where it benefits the researcher everywhere rather than only here. -
Let
boast about orcid:…run directly, expanding and fetching in one command. Rejected: expansion is cheap (one request) but the run it triggers is not — six Providers support papers, so ~118 works is ~700 requests. Putting a mandatory, reviewable artifact between the two makes the expensive step deliberate, and gives the user somewhere to prune before spending it. This is the same reasoning that already makes a Manifest a generated save-file rather than a hand-authored config.
Consequences
-
initis no longer categorically offline. It gains a network path. This is legal — ADR-0001 constrains onlyabout(always-live) andrender/diff(always-offline) — but it is a change in character, soinit’s help text must say so rather than let users assume otherwise. -
Works with neither a DOI nor a PMID cannot become Identities and are skipped. Their count is always written into the generated Manifest’s header, whether or not the user asks to see them listed. Silently shortening the record would understate a researcher’s output — the same failure shape ADR-0002 forbids for Metrics (“we couldn’t look it up” must never read as “it isn’t there”), applied here to a Manifest instead.
-
The generated Manifest must remain valid and runnable as written. Skipped works are therefore emitted as commented-out blocks under
--include-unidentified, never as placeholder identities likedoi:FIXME— a placeholder would make the freshly generated file fail to parse on the very next command, shipping the user something broken by default. -
Because expansion is one-per-work, a large record produces a large Manifest and a long run. That cost is surfaced up front, at
inittime, as a computed warning naming the actual request count — not discovered later whenaboutruns.
Fetches run parallel across hosts, strictly serial within one host
Status
accepted
Context and decision
orchestrator::run used to be a plain nested for identity { for provider { … } } loop — every fetch sequential. That’s fine for one DOI (~6 Providers), but a Manifest expanded from ORCID (see ADR-0006) can hold ~118 works, i.e. ~118 × 6 ≈ 700 sequential requests: a multi-minute floor before RetryingTransport’s backoff makes a bad run worse.
We decided: fetches run concurrently across hosts, but never concurrently against the same host. Crossref and OpenAlex operate “polite pools” that expect a sane request rate from a single client; firing many concurrent requests at one host would get us throttled or blocked — worse than the sequential status quo. Because every current Provider maps 1:1 onto a distinct host, parallelising across Providers yields roughly 6× speedup while staying politer than a naive “parallelise everything” approach: each host still sees requests one at a time, just from several hosts at once.
Concretely, orchestrator::run builds the full Provider×Identity job list up front (same order as the old nested loop), groups jobs by Provider name (standing in for host), and runs a bounded pool of std::thread::scope worker threads that each pull one host’s job queue at a time and drain it strictly in order before picking up another. ureq is blocking, so threads are the natural fit; tokio is unnecessary and would cut against ADR-0004’s minimal-dependency posture. No new dependencies were added. This requires Provider and Transport to be Sync, since fetches now happen through a shared reference from multiple threads.
Considered options
- Parallelise across everything (identities × providers), bounded only by a global limit. Simpler, and would yield a bigger speedup on a Project with many Identities against the same Provider. Rejected: it reintroduces the exact problem this ADR exists to avoid — concurrent requests to Crossref/OpenAlex’s polite pools — for a speedup that isn’t the bottleneck (there are ~11 hosts but rarely more than a handful of same-host Identities in one Project).
tokio+asyncProviders. Would parallelise more cheaply than OS threads at very high fan-out. Rejected: fan-out here is bounded by the number of distinct hosts (~11), not thousands of connections, so async’s main advantage doesn’t apply; it would add a large dependency tree purely for a workload threads already handle well, cutting against ADR-0004.- Unbounded thread-per-job. Simplest possible parallel version. Rejected outright: a 700-job Manifest run would spawn 700 OS threads, and nothing bounds that as Manifests grow.
Consequences
- Snapshot result ordering had to become explicitly deterministic, not just incidentally so. Snapshots are committed artifacts (see the CI-snapshot ticket): if results were serialised in completion order, every run would reorder its JSON and produce spurious diffs, breaking the reproducibility premise ADR-0001 relies on.
runnow pre-assigns each job a slot by its position in the original (identity-major, provider-minor) job list and writes into that slot regardless of which thread finishes when, so the finalresultsorder is always the job order — proven by a test that deliberately reverses completion order and asserts the output order is unaffected. ProviderandTransportgained aSyncsupertrait bound. Every real implementation satisfied it automatically (plain data, no interior mutability) exceptMockTransport’s scripted-sequence replies, which moved fromRefCelltoMutex— an internal change only; its public test API is unchanged.- A Provider’s name is used as its host key, rather than parsing the actual request URL’s host. This is accurate today (every Provider maps 1:1 onto one host) but is an assumption, not an invariant enforced by the type system — a future Provider that fans out across multiple real hosts, or two Providers sharing one host, would silently violate the “never concurrent within a host” guarantee this ADR relies on.
- Concurrency is bounded by a default of 8 (
orchestrator::DEFAULT_CONCURRENCY), user-tunable down viaabout -j/--threads, independent of how many hosts or Identities are involved, so worker-thread count never scales with Manifest size. Raising it past the number of hosts a Project actually touches (at most the Provider registry’s size, ~11 by default) buys nothing — there’s no further axis to parallelise on, since a host’s own queue is always drained strictly serially regardless of the cap. The meaningful direction to tune is down (to1, fully sequential), e.g. to open fewer simultaneous connections on a constrained network;0is rejected by the CLI and treated as1by the library function, since it would otherwise leave every job queued with no worker to run it.
Operational Provider notes are a sibling footer section, not licence notices
Status
accepted — the prose carve-out in Considered options is refined by ADR-0009: what travels into prose is any Metric.note attached to a headline number, not only a licence notice. Extended during #83 so a successful Values Outcome may also carry Provider Notes when requested secondary Metrics were only partly collected; operational messages still stay out of prose. Extended again during #79 (ADR-0011) so a fully successful Values Outcome may also carry a Provider Note recording audit/provenance detail — which CoDex suites a multi-suite aggregation matched — that belongs to the whole fetch rather than any single Metric; this is a third, distinct reason a Provider Note exists (why nothing was collected, why collection was partial, or what a successful aggregation actually drew from), not a widening of “operational.”
Context and decision
report.rs gates every row’s detail text on INLINE_DETAIL_LIMIT (80 characters). Only Metric.note had a fallback past that gate: promotion to the once-per-run ── Notices ── footer (ADR-0005). A NotApplicable/Failed Outcome’s message went through the identical gate with no fallback at all, because provider_notices scans snapshot.metrics() and neither of those Outcomes carries any Metrics. Any such message over the limit was therefore dropped silently from both the terminal and Markdown Reports — the row rendered as a bare N/A/FAILED with nothing saying why. This was found by manual end-to-end testing while implementing the Attention Category, not by a test, and was worked around at the time by shortening the offending Altmetric message to fit under the limit: a fix that left the trap armed for the next Provider to write a longer one.
The obvious repair is to route those messages into the existing footer. We decided against it: a licence notice and an operational explanation are different kinds of text and need different rules. ADR-0005’s footer exists to satisfy Providers whose terms require visible attribution wherever their numbers are displayed, and it de-duplicates by exact text precisely because the same Dimensions boilerplate legitimately repeats across every DOI. Attribution is a property of the data, so discarding Provider×Identity from it is correct. An operational message — “no API key configured”, “rate limited after three retries” — is the opposite kind of thing: it is about one Provider’s attempt on one Identity, and discarding that context is what makes it useless.
The same distinction applies when a Provider returns valid core Metrics but cannot collect every requested secondary Metric. Treating the whole Outcome as Failed would discard real data; putting the explanation on a Metric would misdescribe that number. A Values Outcome may therefore carry Provider Notes describing the partial collection. These notes survive in the Snapshot and Report, but do not by themselves make the default command exit non-zero.
An operational message from NotApplicable or Failed over the inline limit is promoted to a second footer section, ── Provider Notes ── (## Provider Notes in Markdown), rendered after ── Notices ──. A Provider Note carried by Values always goes to that footer because a successful Outcome can contain several Metric rows and has no single honest row to attach collection-wide text to. Entries de-duplicate on (provider, Outcome variant, message). Identity drops out of the key, so one shared message collapses to a single line naming the Identities it covers; Provider and Outcome variant stay in it, so altmetric’s N/A can never merge with crates.io’s, and a NotApplicable can never merge with a Failed — ADR-0002’s honesty model turns on those two staying distinguishable. The row itself is left untouched, with no “see below” pointer, since the footer names its own Identities and the table stays readable without one. Successful Values notes carry no invented status label in the footer.
Considered options
- Extend
provider_noticesto scanNotApplicable/Failedas well. One code path, smallest possible diff, and it does close the drop. Rejected: it mixes operational noise into the one block that exists to be pasted into a grant proposal alongside the numbers, and it inherits dedup-by-exact-text — so a dozen Identities failing would collapse to a single line with no way to tell which, and two Providers emitting the same generic error would silently merge. - Wrap the long text inline beneath its own row instead of promoting it. No footer, no dedup question at all, and Provider×Identity adjacency comes for free. Rejected: a missing API key produces one identical message per Identity, so a run expanded from ORCID (ADR-0006, ~118 works) would print the same sentence ~118 times — the exact wall of text ADR-0005 rejected when it chose a footer over inline notices in the first place.
- Mark the affected row with a pointer to the footer, or with a truncated inline preview. Rejected: the pointer adds a constant string to every affected row carrying information the footer already provides, and the preview duplicates the text in two places while reintroducing mid-word truncation — a bug class this codebase has already been bitten by.
- Surface these in
render_proseanddiff::rendertoo. Rejected for now, on separate grounds. Prose is the automatically written sentence for grant writing and deliberately carries only headline numbers plus the licence notice attached to them, so operational caveats do not belong in it.diff::rendernever renderedNotApplicable/Failedmessages at any length, so extending it is a feature gap rather than part of this drop, and is tracked separately. - Print partial-success warnings only to stderr. Rejected: Snapshots are durable and render offline, so the explanation would disappear when the Snapshot was later rendered or shared.
- Turn a partial
ValuesOutcome intoFailed. Rejected: valid core Metrics would be discarded, and #83 deliberately keeps best-effort Cohort ranking as a successful default command.
Consequences
- Reports have two footer sections with deliberately different semantics: a
Metric.noteover the limit goes toNotices; an operational note carried by any Outcome goes toProvider Notes. A Provider author choosing where their text lands chooses whether it qualifies a Metric or explains collection status, not a section. - Length decides inline-versus-footer for
NotApplicableandFailed, soINLINE_DETAIL_LIMITkeeps the double duty ADR-0005 gave it. A short message from either stays on its row and never reaches the footer, which means a short message shared across many Identities still repeats once per row.ValuesProvider Notes always use the footer because they explain collection across the successful Outcome rather than one Metric row. - The workaround comment on the Altmetric Provider — keep this message under 80 characters — becomes obsolete and should be removed with the fix. It is the only place the trap was written down, so leaving it would keep telling future Provider authors to write around a limit that no longer bites.
Provider Noteenters the glossary as a term distinct fromNotice, so the two are nameable in review instead of both being “the long-text thing”.- The original
NotApplicable/Failedrendering fix did not change the Snapshot. PartialValuesnotes do: the Outcome gains a backward-compatible optional collection with an empty default, and the Snapshot schema version advances. Existing Snapshots without the field remain readable. diff::renderstill shows neither kind of message, so a diff can present an inconclusive entry without theFailedtext that would explain it. Tracked as follow-up work rather than fixed here.
Container pulls roll up, with the caveat travelling
Status
accepted — refines ADR-0002 rule 2, which named a Docker pull as a channel never to be summed
Context and decision
ADR-0002 rule 2 ends: “never sums across incomparable channels (a Conda download ≠ a Docker pull ≠ a git clone)”. Written before any container Provider existed, it settled the question by example rather than by rule, and the example it reached for is the one the Docker Hub Provider (#71) now needs to answer.
The example is right about the facts. A Docker Hub pull is not a conda install: it counts manifest fetches, so CI re-runs, layer probes, and mirror warming all land in the same figure, and the counter never resets — library/ubuntu sits near ten billion. Summing it with a conda count produces a total whose magnitude is driven by machine traffic.
But the clause proves more than it should. By that reasoning a crates.io download is not a PyPI download either — different ecosystems, different retry behaviour, different mirror topologies — yet those have been summed since v1 without objection. Every channel is incomparable to every other at some resolution. What ADR-0002 actually protects is the reader’s ability to see what a total is made of, and the mechanism it built for that is the Rollup’s own construction: a Rollup must name every Metric it includes. A total that names docker:biocontainers/samtools (596335) next to conda:bioconda/samtools (9054107) has not hidden anything.
So the rule is restated in terms of what is actually enforceable:
A Metric may join a Rollup when its Window is compatible and the Rollup names it. A channel whose unit is weaker than the others’ must carry a note explaining how, and that note travels with the total into every format the total appears in — including prose.
Windows still gate summation exactly as before; that part of rule 2 is untouched. What changes is that channel comparability is handled by disclosure rather than by exclusion.
The second half is not decoration, and it is where the original implementation of #71 was wrong. render_prose appended only the headline citation notice, so the sentence read:
As of 2026-08-05, this project has been downloaded 9650478 times (all-time) across 2 channels.
596,335 machine pulls, folded into a grant sentence, with the caveat sitting in a terminal footer the reader of that sentence never sees. Prose is the format that shows a total without the per-channel breakdown, which makes it the one format where the caveat is load-bearing rather than supplementary — and it was the only one where it was missing. Any notice attached to a Metric behind a headline number now travels with it.
Considered options
- Keep ADR-0002 as written and exclude container pulls from the Rollup. Honours the existing text with a three-line change to
counts_as_download, and makes the prose gap moot. Rejected: it treats “incomparable” as a property a channel either has or lacks, which does not survive contact with crates.io-versus-PyPI, and it leaves the reader with two numbers to add up themselves — which they will, without the caveat. - Include, and rely on the Notices footer alone. The terminal and Markdown Reports do show it. Rejected: this was the original #71 implementation, and it is precisely the hole above. A rule that holds in two of three formats is not a rule.
- Make the Provider non-default / opt-in. ADR-0003 lists Docker/Quay among pluggable optional sources, so this has real support in the existing record. Rejected for now: no non-default Provider registry exists yet (
providers/mod.rsnotes theDEFAULTcolumn reads “yes” throughout for want of a contrast), and building one to dodge a disclosure question is the wrong order. Revisit if an opt-in tier is built for other reasons. - Give the Rollup a notion of rollup-ineligibility separate from Window. Most expressive: a Metric could be shown under Downloads yet marked as never summable. Rejected as speculative with one caller — and it would have re-answered the comparability question as exclusion anyway.
Consequences
-
ADR-0002 rule 2’s parenthetical no longer holds as written and is amended in place to point here, rather than being left to contradict shipped behaviour. Its substantive requirement — Windows gate summation, a Rollup names its members — is unchanged and is what this ADR leans on.
-
render_prosenow appends notices from the headline downloads Metrics as well as the headline citation Metric. This is the ADR-0005 rule applied consistently rather than a new one: a notice attached to a quoted number follows it into every format. -
ADR-0008’s “operational caveats do not belong in prose” still stands, but its wording narrows too far and is amended to point here. The split is by Outcome kind, not by content: a
Metric.noteon a real Value is a Notice and travels; aNotApplicable/Failedmessage is a Provider Note and does not. ADR-0008 described the travelling kind as “the licence notice”, because Dimensions’ licence text was the only instance then in existence. A caveat qualifying a number that was collected is the same kind and travels for the same reason. -
This changes prose for one existing Provider, deliberately. PyPI attaches a note explaining that pypistats’ “last month” bucket is treated as a trailing 30 days because no exact day boundary is published. That note now follows the figure into prose, where the sentence says “last 30 days” without qualification. It is the same class of defect this ADR was written to fix, found by the same reasoning, so it is fixed rather than grandfathered.
-
A Provider adding a weak-unit channel now owes a note, not just a Metric.
docker_hub.rsis the worked example. There is no mechanism forcing this, so it is a review obligation — the same status as the Category a Provider picks. -
long_notesbecomes the single definition of “a note long enough to be a notice”, shared by the whole-Snapshot footer and the new headline-scoped prose path, so the two can never disagree about what qualifies. -
Nothing about the Snapshot changes. Notes were always recorded on their Metrics and always survived to the JSON; this was a rendering gap. No schema bump, and existing Snapshots re-render under the new rules.
An unreadable channel is NotApplicable, not Failed
Status
accepted — refines ADR-0002 rule 1, whose Failed examples include “missing key”
Context and decision
ADR-0002 rule 1 defines the three-state Outcome, and lists what belongs in each: NotApplicable is “the Identity legitimately has no presence on that channel”, Failed is “a transient error: rate limit, timeout, 5xx, missing key”.
The Quay.io Provider (#72) forced the question that list papers over. Quay answers 401, never 404, for any repository an unauthenticated caller cannot see — verified live against a missing repository, a missing namespace, and by construction a private one. The three are deliberately indistinguishable, so the registry cannot be enumerated. There is no 404 to key off.
Read literally, ADR-0002 sends that 401 to Failed: it is an auth response, and for a genuinely private repository “the number exists but wasn’t retrievable” is precisely true. But Failed makes two claims that are both wrong here, and rule 3 turns the second into user-visible damage:
- That retrying might work. It cannot.
RetryingTransportdoesn’t retry 401 (correctly — it isn’t transient), and boast holds no Quay credential to retry with. The Provider is keyless by design; there is no configuration in which this 401 becomes a number. - That the Snapshot is partial. Rule 3 exits non-zero when any
Failedremains, soboast about --package quay:biocontainers/typowould report a broken run — and, worse, a Manifest naming one package not published on Quay would permanently fail every scheduled run that includes it. A package’s absence from a registry is not a defect in the fetch.
So the rule is restated in terms of what the caller can act on:
Failedmeans the number is retrievable and this attempt didn’t get it. Where no retry and no available configuration could ever yield a number, the Outcome isNotApplicable, whatever status code carried that news — with any ambiguity disclosed in the note.
This is not new behaviour so much as the existing practice written down. Altmetric already classes a missing ALTMETRIC_KEY as NotApplicable (altmetric.rs’s NO_KEY_NOTE), and CONTEXT.md’s Provider Note glossary entry already gives “no API key configured” as a NotApplicable example — directly contradicting ADR-0002 rule 1’s own parenthetical. The contradiction has been latent since v1; Quay is the first case where it changes an Outcome.
The disclosure half is load-bearing. Quay’s 401 genuinely is ambiguous, so the note says so rather than asserting an absence it cannot verify: “no public repository on Quay.io (Quay answers alike for a missing or a private repository, so this may be a private image)”. NotApplicable here claims only that there is no public presence — which is exactly what was observed.
Considered options
- Send the 401 to
Failed, per rule 1’s literal text. Honours the ADR as written and needs no code. Rejected: it marks every run touching a package that simply isn’t on Quay as partial, and invites the user to retry something that can never succeed. It would make the exit code — the thing rule 3 exists to keep meaningful — fire on a non-event. - Distinguish missing from private before classifying. Most honest in principle. Rejected: impossible by construction. Quay’s anti-enumeration behaviour exists specifically to deny this distinction to unauthenticated callers, and boast is an unauthenticated caller by design.
- Add a fourth Outcome state (e.g.
Unreadable). Expressive, and would keep rule 1’s wording intact. Rejected as a large blast radius — Outcome is serialized into every Snapshot (a schema bump), matched in the orchestrator, both renderers, anddiff— for one Provider’s status code, and it would still have to pick an exit-code behaviour, which is the actual question.NotApplicableplus an honest note already answers it. - Give the Provider an optional
QUAY_TOKENso private repos resolve. Would make the 401 a real “missing key” and put it back under rule 1 legitimately. Rejected as out of scope and low value: boast measures public reach, and a private image has none to measure by definition.
Consequences
- ADR-0002 rule 1’s “missing key” example no longer holds as written and is amended in place to point here. The three-state model and the never-coerce-to-0 rule are untouched — this is about which of two existing states a case lands in, not about inventing leniency.
- A Provider may now classify a non-404 status as
NotApplicable, where the status means “you will never see this”.quay.rsis the worked example;provider::classify_statusremains the default for everything else, and still sends unrecognised statuses toFailed. Nothing is relaxed by default: a Provider has to opt in, deliberately, per status. - The exit code keeps meaning what rule 3 says it means. A partial Snapshot is still distinguishable from a complete one — this change removes a class of false positive from that signal rather than weakening it.
- The risk is a genuine outage misread as absence. If Quay were to start 401-ing broadly — auth-gating the API, or rate-limiting via 401 rather than 429 — every lookup would report N/A with a zero exit code, and a reader would see “not published on Quay” when the truth is “Quay is closed”. This is the real cost of the decision, accepted because the alternative misclassifies the common case to protect against a hypothetical one, and because the note keeps the reason visible in the Report either way. Worth revisiting if Quay’s behaviour changes.
NotApplicablenotes now carry disclosure obligations, not just a reason. Where a Provider cannot distinguish absence from invisibility, the note says so. A note that flatly asserted “not found on Quay.io” would be stating something the response never established.
Optional Providers, and Galaxy CoDex over live per-tool dashboards
Status
accepted
Context and decision
Issue #79 asked for Galaxy usage — tool executions, unique-user accounts, and public-instance availability. The v1 spec already anticipated this shape (docs/spec/0001-boast-v1.md: “further Providers are optional and enabled via Manifest/flags”), but no non-default registry existed yet: providers/mod.rs’s render_providers DEFAULT column read “yes” throughout “for want of a contrast” (ADR-0009), and ADR-0009 itself considered making Docker Hub opt-in and rejected it only “for now”, explicitly flagging “revisit if an opt-in tier is built for other reasons.” Galaxy is that reason: most Projects have no Galaxy wrapper at all, so fetching it by default would add a mostly-empty request to every run rather than a useful one, in a way Docker Hub and Quay (most bioinformatics tools do have a container) do not share.
Two decisions follow from that, both settled in the issue’s own long triage thread before any code was written.
1. Providers can now be optional, disabled by default. providers::optional_providers() is a second registry, disjoint from default_providers(); a user opts a Provider in with --enable-provider <name> (repeatable, unknown name is a CLI usage error) or a Manifest’s enable_providers list. boast providers renders both registries in one table, default rows marked “yes”, optional rows “no” — the exact contrast ADR-0009 was waiting for. An explicit CLI --enable-provider overrides a Project’s Manifest selection entirely (never merges), the same override rule --topic already applies to Cohort selection, so the two stay consistent for a reader who already knows one of them.
2. Galaxy CoDex’s communities/all/resources/tools.json is the source of truth — not the Galaxy Europe Grafana dashboard, and not ToolShed download counts. Both alternatives were live options in the issue thread and both were ruled out on evidence, not preference:
- The Galaxy Europe Grafana dashboard (
stats.galaxyproject.eu) reports numbers roughly an order of magnitude higher than CoDex for the same tool (267 vs. 104 runs for rasusa) — “something is different about how the two collect stats,” never fully diagnosed. It also only covers one server (usegalaxy.eu), not the cross-server picture the issue asked for, and it’s a live query interface rather than a bulk-fetchable dataset. - ToolShed downloads count wrapper-bundle fetches — an installation/update event, not a tool execution. It measures how many Galaxy admins installed a wrapper, not how many researchers ran it, which is the opposite of what “Galaxy usage” was asked to mean.
CoDex republishes one JSON file cross-server, keyless, and reflects the recommended metric shape the issue settled on (execution counts and account counts, not installs). Its own known weakness — the published figures can lag the live servers by an unpublished amount — is disclosed on every Metric via a note long enough to reach the Notices footer (ADR-0005), rather than silently presented as current.
A second wrinkle CoDex forces: one upstream tool can have several Galaxy wrappers, one per subcommand (“suites” in CoDex’s vocabulary — vcflib alone maps to 23). The issue’s own resolution: sum runs across every matched suite, but take the maximum user_accounts across them rather than summing, since the same account can run more than one subcommand and summing would inflate a “how many people” figure into “how many (person, subcommand) pairs.” public_instances unions availability across the matched suites, naming which of the four major public servers (UseGalaxy.org, .org.au, .eu, .fr) matched. Every matched suite ID is recorded in a Provider Note so the aggregation is auditable, and every repository CoDex’s Homepage field doesn’t exactly match is NotApplicable, never a real zero (ADR-0002) — Homepage is deliberately the only field compared; CoDex’s Suite source names the wrapper repository (typically under galaxyproject/tools-iuc), not the upstream Project being measured, and using it would misattribute one repo’s Galaxy usage to a completely different upstream.
Considered options
- Keep every Provider in one always-fetched registry, and make Galaxy request-cheap enough not to matter. No new architecture. Rejected: the cost isn’t request count, it’s relevance — a
NotApplicableon every run for the ~99% of Projects with no Galaxy wrapper is noise in every default Report, not a performance question a cheaper request would fix. - Query the Galaxy Europe Grafana dashboard instead of CoDex, since it’s the most current data. Rejected on the evidence above: an unexplained order-of-magnitude discrepancy against CoDex, single-server-only coverage, and no bulk endpoint — a live dashboard built for humans clicking through a UI, not a source
boast aboutcan fetch once and record provenance for. - Sum
user_accountsacross every matched suite, matching howrunsaggregates. Simpler, one code path for both figures. Rejected: the issue’s own domain expert flagged this would double-count one account using several subcommands of the same underlying tool — the max is the honest “at least this many distinct accounts” figure; the note says so explicitly rather than letting the number imply more precision than it has. - Treat
Suite source(the wrapper repo) as an acceptable secondary match whenHomepageis absent. Would raise the match rate. Rejected: it would attribute usage to whichever team happens to maintain the Galaxy wrapper (oftengalaxyproject/tools-iuc, a shared multi-tool repo) rather than the upstream Project actually being measured — a wrong answer is worse thanNotApplicablehere (ADR-0002).
Consequences
providers::optional_providers()andresolve_optional_providers()are the durable extension point for every future non-default Provider, not a Galaxy-specific mechanism — the next opt-in Provider (Altmetric already asks for a key, but stays default/keyless-degraded rather than opt-in; a genuinely optional source like a paid API tier would fit this registry instead) adds one entry tooptional_providers()and is done.enable_providersjoinstopics/priority_topicsas a Manifest field with the same override shape: CLI-given always replaces a Project’s Manifest value, never merges with it. A reader who understands one already understands the other.- A Category gained a fifth member, Usage, sitting between Downloads and Citations in every Report format and
boast providers’ grouping —CATEGORY_ORDERis the one place this ordering is declared, shared byreport.rsanddiff.rs. The Snapshot schema version advanced (2 → 3) for the new Category, though nothing about deserialising an older Snapshot changes: it simply never contained a"usage"-tagged row. - Galaxy’s
runsanduser_accounts— the two cumulative counts CoDex’s own snapshot can go stale on — each carry the lag caveat on the Metric itself, not just a one-time warning, because Snapshots are re-rendered offline later (ADR-0001) with no re-fetch — the same reasoning ADR-0005 already established for Provider licence notices applies to a data-freshness caveat just as much as a legal one.public_instancesdoesn’t carry it: installation is a current-state fact CoDex derives the same way regardless of refresh timing, not a count that accumulates staleness. - A future Provider facing the same “several wrappers, one upstream” shape has a worked precedent: sum what genuinely aggregates (executions), take the maximum of what would double-count under naive summation (accounts), and say which rule applied to which number in the Metric’s own note rather than leaving a reader to guess.