Open-source Clinical Study Report builder

Versioned numbers, versioned words, one traceable loop.

Every number in an open.csr report is computed by a versioned spec, serialized as an Analysis Results Dataset, rendered into a display, and bound — never retyped — into the sentence that quotes it. Change the request, regenerate the number, and the prose moves with it as one transaction.

Open the demo reportSee the evidenceRead the design

The demo is one view with four ways in — the assembled report, the table and ARD behind any number, the prose that quotes it, and the ICH E3 model it all assembles into.

6displays registered6 generated
15text blocksbinding numbers, not stating them
265reviewed requirements11 matrices
441test recordsjs-unit · r-unit · text-review

The gap open.csr occupies

Open source owns the numbers

pharmaverse, NEST and the R Consortium pilots generate submission-grade tables — and stop at the output object.

Commercial owns the words

CoAuthor, Yseop, TriloDocs and Narrativa assemble documents — and treat the TFL package as an opaque, already-final input.

Both halves break at the same seam: prose quotes numbers the authoring tool did not compute. Cross-checking numbers against tables is a patch over that seam. open.csr closes it.

The closed loop

  1. 1Change requestA reviewer asks for a risk-difference column on the AE overview.
  2. 2Spec diffThe agent proposes an edit to analysis.yaml — reviewable, small, versioned.
  3. 3RegenerationThe pipeline writes a new outputs/<slug>/vNNN/: spec snapshot, ARD, display, manifest.
  4. 4Re-bindingEvery sentence that binds a number from that display re-resolves against the new ARD.
  5. 5GateCI diffs the ARD, re-checks numeric fidelity, and republishes the evidence.

One transaction, fully versioned, every artifact reproducible from the commit. Agents write source; humans approve; the pipeline is the only thing that regenerates.

Architecture

 pharmaverseadam ──▶ data-prep ──▶  ARDs  ──▶ displays ──▶ assembled CSR ──▶ demo site
      (ADaM)        (R, tested)    (cards)   (gt/tfrmt/    (Node, from       (GitHub
                                              r2rtf)        the E3 model)     Pages)
                         ▲            ▲           ▲              ▲
                library/tfl/    library/tfl/  library/text/  library/templates/
                analysis.yaml   display.yaml   text blocks    ich-e3/

 ┌──────────────────────────────────────────────────────────────────────────┐
 │  quality/ — requirement matrices · testthat + vitest evidence · guards    │
 └──────────────────────────────────────────────────────────────────────────┘

TFL Builder + Library

library/tfl/<slug>/

  • analysis.yaml — what to compute (ARS-aligned)
  • display.yaml — how to show it
  • iterations.yaml — every saved regeneration

Text Library

library/text/<ID>.md

  • Three tiers: boilerplate, parameterized, generated
  • Numbers arrive as {{ard:…}} bindings
  • Approval state travels in the frontmatter

Report Template Library

library/templates/ich-e3/

  • ICH E3 as data: 16 sections, content models
  • Assembly slots and 14.x numbering at build time
  • 16.1.9 provenance appendix generated mechanically

Traceability, end to end

Every number on this site answers five questions — which dataset, which spec, which ARD row, which display, which sentence.

adam (pkg version + hash) → data-prep manifest → ard.json (rows + envelope)
  → display vNNN (manifest: spec hash, commit) → CSR slot (assembly.yaml) → text binding

The CSR Reader renders that chain as a trace panel: click a bound number and the whole provenance path opens beside it.