gsm.safety release review 2026-08-22

What v1.3.0 corrects, annotated

A safety overview leads with denominators — how many participants, how many dosed, how many died. gsm.safety computed those inside one function, from whatever columns it was handed, and got several of them wrong. This release replaces every one with a metric that publishes its own numerator, its own denominator and a record of where the figure came from. Both versions were run for this page on 2026-08-22, on the same study, in the same session.

deaths 4 → 13 of 762 5 published figures move 12 census metrics, each qualified twice candidate gs#69, release/v1.3.0main

the headline · gs#56

The death count

On the ecosystem's bundled study, the safety overview reported four deaths. Thirteen enrolled participants have a death recorded. The old count never opened the death domain at all: it matched the text of a discontinuation reason and counted whoever it found, enrolled or not.

Measured for this page on 2026-08-22 by two routes that share no code — the study's records read directly with base R, and the standard mapping run through the metric workflow end to end. Both give 13.
Where a death is recordedParticipantsOf them enrolled
The death domain1210
Discontinuation reason reads "Death"43
Named by both00
Union1613

Three of the sixteen were never enrolled — S42425, S97688 and S78705 — and the metric anchors every figure to the enrolled population of 762, so the published number is 13. The old count did not anchor, which is why it reported all four of its matches including the one who was never enrolled.

The correction is four to thirteen, and it is stated that way deliberately

An earlier version of this work said the function reported one death, and that version circulated for about a day before being corrected. One is what it reported on gsm.core 1.2.0, where a single participant's discontinuation reason said Death. The bundled study moved between gsm.core versions under the same name: on 1.3.1 four participants carry that reason, and 762 are enrolled rather than 760.

So this is a threefold correction to a published clinical figure, not a thirteenfold one. The metric's own figure of 13 was re-measured on 1.3.1 and did not move.

The same code, run twice for this page: once against this machine's own R library, once against the versions the release was qualified on.
The bundled study AA-AA-000-0000gsm.core 1.2.0gsm.core 1.3.1
Participants in the subject domain10001000
Enrolled760762
Discontinuation reason reads "Death"14

This is the trap the release exists to close, and it is also the reason this page was not built on the machine's own library: rendering the census here would have republished the superseded figures.

gs#56 · gs#58 · every figure that moves

The census, before and after, in one session

Both runs call SafetyCensus() on the same mapped domains from the same study, minutes apart: gsm.safety 1.2.0 first, then 1.3.0. Nothing below is copied from the release notes or the qualification records — those were read afterwards, to check this run against them.

Figure on the safety overviewv1.2.0v1.3.0What the old number was doing
Enrolled participants762762unchanged
Randomised to an armblank577read a treatment-arm column no standard domain carries
Received study drug744762inferred dosing from time on treatment exceeding zero
Deaths413matched a discontinuation reason, never read the death domain
Person-years on study73.273.2unchanged
Person-years on treatment29.529.5unchanged
Participants with a lab result598598unchanged
Participants with an ECGblank, of 762absent, and it says sopublished a blank where the domain is missing
Participants with a reported AE661661unchanged
Participants with a disposition record10076counted every identifier in the domain, enrolled or not
Completed2219counted the same way, and inside a table rather than as a figure
Discontinued109counted the same way, and inside a table rather than as a figure
Median days on treatment15no longer publishedwants an averaging step no metric performs yet
Ongoing / Not in the disposition domain64 / 662no longer publishedread out of free text, and a subtraction

Two figures left the page rather than moving, and they are named rather than dropped

  • The median days on treatment needs an averaging step in the metric layer. Publishing it from the report would put a second counting lane back, which is the defect this release removes.
  • The per-visit data-coverage table is the thirteenth census figure and is carried. Under the standard mapping the lab domain arrives with no visit column at all, so a coverage figure cannot key on a visit — and the expected count needs a scheduled study day no domain supplies. The page renders with no coverage section rather than an empty table, because an empty table reads as a study with no data.

Every figure that is published was measured twice, by routes that share no code, and the pair has to agree or the script exits non-zero. Run either yourself from the branch:

# the records read directly with base R, against the pipeline, figure by figure
Rscript tools/qualify-census-metrics.R
Rscript tools/qualify-death-count.R

#   AGREE - every figure measured twice, and saf0011 stops rather than publishing a zero.
#   AGREE - 13 participants, of 762 enrolled.

Both were run for this page and both agreed. Every row measured matches what inst/qualification/ records, including the eleven figures in the metrics record and all four counts in the death record.

gs#61 · the page it produces

The census report, rendered

One report, reading what the metrics published and computing nothing of its own. Rendered for this page by running the whole pipeline — the standard mapping, the eleven census metrics with a domain on this study, the reporting model, then the report workflow.

The rendered Safety Census page for study AA-AA-000-0000: Population, Exposure and Follow-up sections, each figure shown with its value, its denominator and what the denominator counts; then a What the metrics published table listing each metric ID with its numerator, denominator and status, including saf0011 marked not run for this study.
  • Every figure sits beside the denominator its own metric published, and beside what that denominator counts.
  • Person-time is published in days and presented in years — both numbers are on the page.
  • The foot of the page lists what each metric published verbatim, including the one that published nothing.

The page carries no flag column and no cut-point. These metrics declare no threshold and publish an empty flag, so a census figure cannot move a site's risk score — and a result arriving with a flag is refused rather than presented.

Why it matters

A figure that is wrong is now wrong in exactly one place. Before, the same count could be produced by a function and by a metric and the two could disagree without anything noticing. Now the function runs the metrics and reads what they published, so there is one counting lane and the page is a reader of it.

The test that proves it is not the one that checks the numbers. A structural check reads the function's body and every helper it calls, and fails if an arithmetic operator or an aggregating function appears in any of them. A rebuild that left the counting in place and put a workflow beside it would pass an arithmetic test by accident; it cannot pass that one.

Try it

  1. Compare the top table with the foot of the page: 73.2 person-years above, 26 754 participant-days below, the same figure twice.
  2. Find saf0011 in the foot: it reads "not run for this study" rather than reporting a zero.
  3. Read the denominator column — every figure is of 762, and the page says what 762 counts.
gs#66 · absent is not zero

What happens when a study does not collect something

A zero means measured and found none. A blank means the reader has to guess. Both were being published where the honest answer is that nothing was collected. Run for this page: the same call, the same study, with the death domain withheld.

# the death domain supplied
Deaths                     13  of 762

# the death domain withheld — the same call, everything else identical
Deaths                     NA

# and it says so, rather than leaving the reader to notice:
Warning: No domain was supplied for Deaths (Study) (Mapped_Death);
         Participants With an ECG (Study) (Mapped_EG).
         Those figures are absent rather than zero.

Three states, kept apart, and checked in the strongest form available

  • Every column every census definition declares is dropped in turn, and the metric has to stop rather than publish — 34 combinations across the eleven metrics, plus four for the death count.
  • This matters because the underlying check only warns on a missing column: it stops on a missing data frame, but a domain arriving without a column it needs would otherwise sail through. That was verified again on gsm.core 1.3.1 rather than taken from the design, which had called the false-zero problem fixed by construction.
  • The ECG metric is written, declares its domain, and stops on every study available today — no mapping package ships an ECG mapping and the bundled study has no ECG records. Its qualification is structural rather than measured, and the suite fails if a bundled ECG domain ever appears, so it cannot sit there passing on nothing.

On a study that maps no death domain, the death figure now reads as not collected instead of reading a discontinuation reason. That is correct, and it is also a visible change: the demo study's deaths and randomised tiles will read as not collected until its mapping phase adds those two domains.

notes

Reading this page

What produced the numbers

  • gsm.safety 1.3.0, built from release/v1.3.0 at 9f76d42, and gsm.safety 1.2.0 from release/v1.2.0 at 4a436ce for the before column.
  • gsm.core 1.3.1 and gsm.mapping 1.1.6, built from the Gilead-BioStats main branches into a scratch library, with gsm.reporting 1.1.5. Both main branches are byte-identical to their release tags, checked while writing this page, so this is what CI installs.
  • R 4.3.3, on 2026-08-22. Every figure comes from one of those runs; the qualification records were read afterwards, to check the run against them, and every row agreed.

Also in the release, not shown here

  • The qualification records now ship inside the package rather than in a directory the build excludes, so the checks that compare them with the code run everywhere the suite runs — including the package check the merge is gated on, where they had been silently skipping.
  • Every document-agreement check prints a line naming the record and how many figures it compared, so a skipped check is visible in a log rather than being one line inside a list of sixteen.
  • Ten column arguments are deprecated and ignored rather than removed: a call that passed one still runs, warns, and changes no figure.

What is deliberately not finished

  • Data completeness, the thirteenth census figure, is carried with the reason written down: the lab domain carries no visit column under the standard mapping, and the expected count needs a scheduled study day no domain supplies. Both are definitional choices rather than code to write.
  • Migrating off two deprecated pipeline functions is not in this release at all — its 30 warnings are still visible in this branch's logs — and it no longer carries this release's milestone.
  • The counts pinned in the suite are pinned to gsm.core 1.3.1. A different version skips those assertions with both numbers named in the skip reason, so re-qualifying is a deliberate act by a person. Worth knowing: the development branch of gsm.core already carries a much larger bundled study, so the next release will move it a third time.

Where this fits

This page is the review surface for gs#69, which promotes release/v1.3.0 to main and tags v1.3.0. gsm.safety is a clinical repo: nothing reaches main without your review.

It is the second of two candidates and contains the first, so gs#68 has to merge and be tagged before this one. Merging them the other way round swallows the widget release.

The rest of the candidate

  1. The qualification records — three, shipping inside the package
  2. D0023 — the design, carrying its own dated correction about the study that moved
  3. The v1.2.0 demo — the candidate this one is stacked on