You said the census stays and is rebuilt: the numbers become metrics, the census becomes a report, and the shape is workflows calling helpers rather than one large function. This is that design. It is built around your sentence that a metric earns its place by being a number you can validate, whether or not anything fires on it — and it turns out the ecosystem already ships a metric like that, so the idea needs no new machinery. Six things it could not decide without you were at the bottom, and on the evening this page went up you answered all six by approving the recommendations. What each one now commits the build to is read out immediately below.
Building the first of the thirteen metrics (gsm.safety#56, merged) put every figure on this page through a second route. Three did not survive. What he decided stands unchanged — what moved is the arithmetic it was argued with, and one figure he approved by name.
Every number here from the ecosystem's bundled study was measured against gsm.core 1.2.0. The package is now 1.3.1, and the study is different data under the same name.
| Figure | As measured here (1.2.0) | As it reads now (1.3.1) |
|---|---|---|
| Enrolled participants — the denominator | 760 | 762 |
| Death records | 12 | 12 |
| Deaths named by a discontinuation reason | 1 | 4 |
| Union, all participants | 13 | 16 |
| Union, enrolled only — the published count | 12 | 13 |
The headline figure survives at thirteen, but not for the reason stated. This page reached thirteen as twelve death records plus one discontinuation reason. It is now sixteen participants named by the two sources, three of whom were never enrolled and are excluded by this design's own anchoring rule. Right number, different arithmetic.
His first answer published the two sources' disagreement as a number of its own, and this page argued it as a small data-quality signal — one participant on the bundled study.
On 1.3.1 no participant is named by both sources. The two sources overlap on nobody, so the disagreement is not a nuance on the edge of the count: it is the whole count. Thirteen of thirteen.
That does not undo his answer — publishing the disagreement is more useful, not less, when it is total. It does mean the metric cannot be written until the disagreement is defined against numbers that hold, and the definition is worth his eye rather than an author's guess. Carried as an open question on the requirement.
The findings table below says a declared column that goes missing stops the workflow with an error, and calls the false-zero defect fixed by construction. That is not true of the framework as it stands: gsm.core's spec check errors on a missing table but only logs a warning on a missing column.
Verified directly in the installed package rather than taken from the build. The first metric carries its own guard and stops; the remaining twelve each need the same one written into them, so this is a rule for the build rather than a property inherited from the framework.
This page said the function reports one death. Building the remaining twelve metrics measured it: on gsm.core 1.3.1 it reports four. One is what it reported on 1.2.0 — the same version shift as the rest of this correction — and the function anchors nothing to the enrolled population, so it counts all four participants whose discontinuation reason says Death, three of them enrolled and one never enrolled at all.
The correction the rebuild makes is four to thirteen, not one to thirteen. It is not the thirteenfold change this page described, and the masthead figure above has been corrected in place.
What does not change is the substance: the count was arrived at by matching the text of a discontinuation reason, never read the death domain at all, and counted people who were never in the study. That is the defect, and its size was never the argument.
The before-figure is now asserted by the package's own test suite rather than quoted in prose, which is why this was caught at all.
Sources. The re-measured figures and the two independent routes that produced them are recorded in gsm.safety at design/death-count-qualification.md, with a reproducer that exits non-zero if the routes ever disagree. The framework behaviour was read from the installed gsm.core 1.3.1.
He answered this page the evening it went up, in chat, after listening to its audio episode — one sentence carrying all six questions at once. Each question is read out below in the form he approved it in: what it now commits the build to, not merely that it was approved. A recommendation is a paragraph of argument with a choice at the end of it, and the choice is the part that has to survive.
“I listened to the safety sentence episode and approve the recommendations.”
Recorded on this page the same evening he said it, out of the session that received the answer. The recommendations he approved are the six published on this page when he listened to it; none of them has been edited since, and the wording of each is reproduced under its question below.
He said “safety sentence”, and it is read here as “safety census”. The grounds: the episode of this page is titled Decision: safety census rebuild, it is the only census episode on his show, and it is the episode carrying these six recommendations. That is an interpretation of a dictated word rather than a transcription of it, so his sentence stays above exactly as he said it and the reading is annotated here instead of being quietly folded into the quote. If the reading is wrong, so is this whole section, and nothing under it should be built.
Approved: both, and their disagreement is its own number — the third of the three options, not the cheapest and not the status quo.
gsm.mapping's existing definition, so no clinical definition of death is being written by this program.Death domain and has no Raw_Death.csv today, so that is work in demo-301, outside gsm.safety, and it belongs to this rebuild.Approved: time on study — not the visit schedule, and not leaving it as total enrolment.
timeonstudy is already on the standard subject mapping, so this is computable everywhere the pipeline already runs.Approved: none — including the one plausible candidate, data completeness.
Approved: study only, built so the level is a setting. Both halves are load-bearing; the second half is the part the recommendation was actually asking for.
Approved: nothing breaks — not the shape change, and not deprecate-and-remove.
Enrolled participants, Received study drug, Person-years on treatment and Deaths stay word for word, with a test behind them — the application looks them up by exact string and drops a tile silently rather than erroring if one is reworded.Approved: no chart, and file the chart requirement in the same breath. The second clause is a condition of the answer, so it was discharged as part of recording it rather than left as a follow-up.
gsm.safety is clinical work he reviews before production; approving the design settles what gets built, not that it ships.The safety census is one exported function, two hundred and seventy-seven lines long, that reads five raw tables and returns every denominator a safety reader needs — how many participants, how many dosed, how much person-time, who left and why, and how completely the data was collected. An adversarial review confirmed that several of those figures are silently wrong, most seriously a death count that reports one where the same study's records hold thirteen. You decided it stays and gets rebuilt so that each number becomes a metric the pipeline computes and can be checked on its own, and the census becomes a report over those metrics.
What you said, dictated on 18 August:
“I think that we should model it after the gsm.kri report, where these core numbers that right now are clearly wrong are metrics. Not every metric has to have an action associated with it. So number of deaths should be a metric, not some code wrapped deep in the safety census function. Right now those three safety metrics we have are all, in theory, actionable and flaggable, but that's not the only point of metrics. The point of metrics is to have trustable numbers that we can qualify and validate.”
A metric with no action is not a lesser metric. Everything below follows from taking that literally: the death count, the enrolment count and the person-time totals go into the same machinery as the flagging metrics, get the same specification, the same intermediate tables and the same independent inspection, and simply never raise anything.
The obvious objection to a descriptive metric is that the framework will not take one: the flagging step refuses to run without a threshold, and the step after it insists on a flag column. Both are true, and neither matters, because the risk-score metric in the key-risk-indicator package already goes around them. It declares no threshold, calls no flagging step, and publishes its rows with the flag left empty. It has been doing that in production the whole time.
Three consequences, and they are the reason this is a modest build rather than a framework change:
So a census metric is an ordinary metric definition with the threshold lines simply absent, plus one small helper in our own package that stands where the flagging step would be and marks the row as not flagging. That helper is about ten lines, and it is the only genuinely new mechanism in this design.
Three layers, each one a thing the pipeline already knows how to run.
Each census figure becomes its own metric definition file, named and numbered the way every other metric in the ecosystem is, and each declares the exact columns it reads from the standardised study domains. Declaring them is what fixes the largest class of defect: a column that is missing stops the metric with an error instead of quietly reporting zero.
| The number | Counted from | What changes about it |
|---|---|---|
| Enrolled participants | the subject domain | nothing — already correct |
| Randomised participants | the randomisation domain | stops reading a treatment-arm column that the standard domains do not have, which is why it currently comes out blank on any study but the demo |
| Participants dosed | the subject domain's first-dose date | stops being inferred from treatment time being greater than zero, and stops reporting zero when that column is absent |
| Deaths | the death domain | stops being a text match on a discontinuation reason — see the first question |
| Participant-days on study | the subject domain | published as days, the number actually summed; the report presents years |
| Participant-days on treatment | the subject domain | same |
| Participants with a lab result | the lab domain | counted once per participant, so a duplicated row cannot inflate it |
| Participants with an ECG | the ECG domain | same |
| Participants with a reported adverse event | the adverse-event domain | same |
| Participants with a disposition record | the study-completion domain | same |
| Participants who completed | the study-completion domain | promoted from a row inside a table to a number of its own |
| Participants who discontinued | the study-completion domain | same |
| Data completeness | the lab and ECG domains against the visit schedule | becomes one headline figure with a per-participant, per-visit table underneath it — see the second question |
Every one of them uses the standard rate step, which anchors the count to the enrolled participants and looks the rest up against them. That single choice fixes two separate defects at once: a participant identifier that appears in a disposition record but was never enrolled cannot be counted, and the count and the denominator can no longer disagree, because they come out of the same operation. The published row carries the count and the denominator side by side, which is exactly the pair the census displays.
The census report becomes a report workflow of the same kind the key-risk-indicator report already is: it declares the tables it needs, and its steps call helper functions that assemble and render. It reads the metric results the pipeline produced rather than recomputing anything, so there is no second counting lane and no way for the census to disagree with the rest of the package about the same study.
Two things the metric layer genuinely cannot express stay in the report, and the design says so rather than pretending otherwise:
The function survives as the one-call front door: it runs the census metric workflows over the domains it is handed, assembles the same three tables it returns today, and hands them back. What disappears from it is the arithmetic — every number inside it moves out into a metric definition that can be run, inspected and checked on its own. What survives is the convenience, which is the part anybody actually calls.
The review confirmed each of these by running the code, and none of them is retracted. This table is the promise that the rebuild does not quietly lose any of them.
| What the review found | What the design does |
|---|---|
| The death count reads a discontinuation reason and reports one death where the bundled study's death records hold twelve and the standard mapping's combined figure is thirteen | Fixed, subject to the first question — deaths become a metric reading the death domain, and a study that does not supply one reports no figure rather than a wrong one |
| A missing treatment-time column makes “received study drug” report zero — a false clinical zero, in a function whose own tests certify that absent data is never zero | Fixed by construction — each metric declares the columns it reads, and a declared column that is missing stops the workflow with an error |
| Participant identifiers present in the disposition table but never enrolled push counts past their denominators — five completed in a four-person study | Fixed by construction — every count is anchored to the enrolled participants and looked up against them |
| Duplicate rows double-count person-time and dosed participants | Fixed, with a rule and a test — every count metric reduces its domain to one row per participant before counting, and the test asserts that duplicating a row changes nothing |
| A blank completion reason produces a state labelled with the literal word for a missing value | Fixed in the report's disposition helper — a missing reason says so in words |
| “Randomised to an arm” silently comes out blank on the standard domains, because none of them carries a treatment-arm column | Fixed, and better than fixed — randomisation is counted from the randomisation domain, which records that a participant was randomised without recording to what, so the figure stops depending on a column your blinding stance says should not be read anyway |
| Visit ordering degrades to alphabetical, so week twelve sorts before week two — the exact behaviour the function's own test forbids | Fixed by the second question's answer — coverage keys on the visit schedule rather than on whatever label the lab rows happen to carry |
| The column defaults were proven against one study's bespoke data and match no standard domain | Fixed by construction — the columns are declared against the standard domains and checked before anything runs |
| The documentation describes almost none of the real semantics — that expected-per-visit is just enrolment repeated, that dosed is a proxy, or where the boundary between zero and absent sits | Carried — each metric definition carries its own description, and the report states its denominators; but the writing is work, and it is on the build list rather than solved by the architecture |
| It is the package's only export that is neither a pipeline step nor referenced by any workflow, and its return shape is rejected by the pipeline's own summarising step | Fixed by construction — thirteen metric workflows and one report workflow, all of them ordinary members of the pipeline |
| It is a second, parallel counting lane whose figures already disagree with the pipeline's on the same study | Fixed by construction — the report reads the pipeline's results and computes nothing of its own |
| Eleven column-name parameters have no test coverage at all, because every test runs at their defaults | Fixed by removal — the parameters go, replaced by declared columns on standard domains, so there is no untested configurability surface left to cover |
Documentation. Every other finding is answered by the shape of the design; that one is answered by somebody writing the words, and it is the finding most likely to be quietly dropped because nothing fails when it is. It is on the build list as its own step with its own acceptance: a reader who has never seen the code can tell, from the page alone, what each number counts and when it is absent rather than zero.
The interface barely moves. The numbers move a lot, and that is the real cost of this rebuild, so it is worth being exact about it.
The demo's safety overview reads the census payload live. These figures change the day this ships, and every one of them changes because it was wrong:
The demo application looks up four census rows by their exact wording — enrolled participants, received study drug, person-years on treatment, and deaths. If any of those labels is reworded, the tile does not error; it disappears. That is the silent-success shape this program keeps meeting, so the wording of those four is treated as a contract with a test behind it, and any rewording ships in the same release as the application change.
He answered every one of these on 2026-08-20 by approving the recommendation on each, and the record is at the top of this page. The questions are kept below in the form he read them — the options he was offered, and the recommendation he approved on each — because a recommendation cannot be checked against an answer if the page rewrites itself into the answer.
Six questions. Each had a subject word and named choices, so any of them could be answered out loud without a screen: say the subject word, then the choice. Answering a subset would have been fine — the ones he answered unblock their part of the build. He answered all six in one sentence.
There are three answers in the ecosystem's own bundled study and they are one, twelve and thirteen. One is what the census reports now, by matching the word for death in a free-text discontinuation reason. Twelve is the dedicated table of death records. Thirteen is the standard mapping's combined figure, which unions the two — twelve participants with a death record, plus one whose discontinuation reason says they died but who has no death record at all. That last participant is the interesting one: the sources genuinely disagree about a real person.
The coverage table is the part of the census with no counterpart anywhere else in the ecosystem, and the review judged it the idea most worth keeping: it reports, per visit, how many participants have a result against how many should. The trouble is the second half. Today “should” means total enrolment, repeated at every visit — so a study that enrolled half its participants last month looks like it is missing half its data.
Your sentence says a metric does not need an action. It does not say none of them may have one, and there is one plausible candidate: data completeness. A visit where a large share of participants have no result is a monitoring finding in its own right, and it is the figure the coverage table exists to surface.
The census answers about the study. The report you named as the model answers about sites. The metric framework computes at whichever level it is told, and the ecosystem's usual practice is to write the same metric out once per level rather than adding levels up — because some figures, medians among them, cannot be added up correctly.
Two contracts exist. One is the function's own arguments. The other is the compact file the pipeline writes and the demo application fetches, whose shape and whose row labels the application depends on.
You said the census probably becomes a report, perhaps with charting functions, and that the way charts are done in the two chart libraries today is fine for this. The coverage table is the one part that genuinely wants a picture. Where it gets built is the question, because our own chart library carries a completion bar: a new chart is not finished until it has a gallery demo, an evidence page, an interface reference and a binding in the R package.
Nothing was blocked on all six, and in the event all six were answered at once — so nothing on the list above is waiting on him any longer. What the build now waits on is his review of the implementation itself, which is a separate gate: gsm.safety is clinical work and he sees it before production.
Four steps, in this order, each landing as its own piece of work under the requirement. The order matters: the death count comes first because it is the number the review was written about, and until it is right nothing else about this rebuild has been demonstrated.
How it is checked is not a matter of the tests passing. Each number is checked against the same study through a second route — the death count against the death records read directly, the enrolment count against the subject table, the coverage figures against the raw domain — and the check is recorded. That is what qualifying a number means, and it is the part your sentence about validation is actually asking for. A test that the code does what the code does would not have caught any of the defects in the table above.
The published figures change when this ships, and the largest of them is a death count going from one to thirteen on a public page. Under your transparency rule the release notes name every figure that moved and why it was wrong before — not as a footnote, as the substance of the release. A safety tool quietly correcting a death count is worse than one that never had the bug.
Sources. The function, its tests and the package's three existing metric definitions read from gsm.safety at its published state. The death figures reproduced for this page by running the standard mapping against the ecosystem's own bundled study: one from the discontinuation reason, twelve death records, thirteen combined. The unflagged-metric precedent read from the key-risk-indicator package's risk-score definition and the builder that excludes it from scoring. The standard domain definitions read from the mapping package as installed. The demo study's calling script and its application's census reader read from their repositories, including the four row labels the application looks up by exact wording. The verified defects are the review panel's, reproduced on this page rather than restated: seven independent reviewers and seven fact-checkers, forty-seven claims, none refuted.
References. Decision this design answers: SafetyCensus stays or goes. Requirement: obot.roadmap#274. This design: obot.roadmap#284.
Drafted by 👯🤖 W0085 using Claude Opus 5. Not reviewed by @jwildfire before publication — he answered it on 2026-08-20 after listening to the audio episode, and his answer is recorded at the top of this page. His decision was recorded here by 👯🤖 W0096 using Claude Opus 5, the same evening.