Try it: https://robterheine.github.io/nephrotdm/, or download nephrotdm.html and open it in a browser (it works from file://, offline).
Research use only. Not a medical device. The app never suggests, recommends or optimizes a dose or interval.
Read this file before touching anything. It is the cold-start handover for this app. After reading it, an AI agent or a person should know what the app is, what it must never do, how it is built and how to change it safely.
What it is now (v1.5.2). Five population PK models for kidney transplantation, one card each: mycophenolic acid for adults (de Winter 2008) and for children (Heida 2024), tacrolimus for adults (Størset 2014) and for children (Schijvens 2020, refitted in Heida 2026), and everolimus for adults (Zwart 2021, Model 3). For one patient, each estimates the steady-state AUC₀–₁₂ₕ and a trough with 5–95% intervals, from the covariates and the samples entered. Tacrolimus and everolimus values are also shown corrected to a reference haematocrit. The window the probabilities refer to is the clinician's input.
How well it is checked. The closed-form engines match NONMEM 7.6 (structure to 5e-9 and POSTHOC for every drug, and a Bayesian comparison for adult MPA and the pediatric models). Every model has a calibration study. The pediatric models were also reviewed by three independent seats (pharmacometrician, UI designer, clinical pharmacologist), each with the verdict "pass with comments". No named human has signed
docs/VERIFICATION_TEAM.mdyet. Open points are in §11.Form factor. A single-file, dependency-free, offline HTML app. The deliverable is
nephrotdm.html(about 625 KB with the embedded fonts), generated bybuild.mjsfromindex.htmlandsrc/*. It wasmpa-tdm.htmlup to v1.1.1; that old file was deleted in 1.5.1. It must keep working when opened fromfile://: no server, no CDNs, no npm packages in the artifact, and no network needed (the one exception is the page-view counter on the published site, rule 2).Where the detail is. Release notes per version (
docs/RELEASE_NOTES_V*.md); one hand-off per added model (docs/HANDOFF_EVEROLIMUS_M3.md,docs/HANDOFF_PEDIATRIC.md); the evidence next to them (docs/CALIBRATION_RESULTS*.md,docs/NONMEM_CROSSCHECK*.md,docs/VERIFICATION_*.md,docs/REVIEW_PEDIATRIC_*.md); the source readings (docs/SOURCE_ANALYSIS_*.md,docs/MODEL_ANALYSIS_*.md).History in short.
- 1.5.2 adds an anonymous visit counter on the published site only (GoatCounter, an image request, rule 2 and
docs/RELEASE_NOTES_V152.md). Nothing else changes.- 1.5.1 answers the adversarial review of 13 October 2026 (
docs/ADVERSARIAL_REVIEW_V150.md, every finding and what was done indocs/ADVERSARIAL_FIXES_V151.md): a mistimed dose can no longer turn into a silent 6-fold AUC, the report is tied to the run it describes, the haematocrit is read from the latest sample, a convergence check that passed on NaN was fixed, session import is stricter, Ctrl/Cmd+P prints the report, and the report fits one page on Letter and A4. Details indocs/RELEASE_NOTES_V151.md.- 1.5.0 adds the two pediatric models (§14). The adult drugs are unchanged: bit-identity records V16, V16b and the everolimus record, a byte-level snapshot of the MPA and tacrolimus UI text, and a browser comparison with 1.4.0.
- 1.4.0 adds everolimus (§13), the one-page A4 report, the visual refresh with embedded IBM Plex fonts, and the field-fit audit.
- 1.3.0 adds adult tacrolimus (§12) and renames the app NephroTDM. (1.2.0 and 1.2.1 were never published.)
- 1.1.x is the de Winter 2008 MPA model going live, together with the result of the methods audit: closed-form propagation and exact steady state (about 0.5 s per fit), a converged sampler with R̂/ESS diagnostics, the EC-MPS membership-sampler fix, and the removal of the assay, LLOQ, error-multiplier and IV inputs (censored samples are not supported). Evidence:
docs/METHODS_AUDIT_V101.md,docs/CALIBRATION_RESULTS.md,docs/NONMEM_CROSSCHECK.md,tests/golden.json,tests/nonmem_golden.json.Published. The app is at https://robterheine.github.io/nephrotdm/ (repository
Robterheine/nephrotdm,main, onlyindex.html, which is the builtnephrotdm.html; Pages from the root). The source is inRobterheine/nephrotdm-source(main), the same layout ascomplementtdmandcomplementtdm-source. The earlier MPA-only page (mpatdm) was retired. To republish: edit this folder, runnpm testandnpm run build, copy the project into a clone ofnephrotdm-source(leaving out.git,node_modulesanddocs/AUTHOR_QUERY*), copy that clone's freshly builtnephrotdm.htmltoindex.htmlin a clone ofnephrotdm, then commit and push both. Never push without the owner's go.
Is: a clinician-facing TDM tool for kidney transplantation. For one specific patient it estimates the steady-state AUC₀–12h with posterior uncertainty, the AUC over a 12-hour dosing interval at steady state (a regimen with another interval is reported as its 12-hour equivalent, AUC₁₂ = AUC₀₋ₓ × 12/x). The input is an established population PK model plus limited sampling, for example a 3-point schedule. The app shows the probability that the exposure lies inside a window with both a lower and an upper bound, and lets the clinician explore what a different maintenance dose would do, at the fitted interval or a chosen alternative (12 or 24 h), using that same fitted posterior. The windows are the clinician's input; the app takes no position on them.
Every drug also reports a trough. For MPA it is shown for information only, because there is no trough target (the IATDMCT consensus says trough correlates poorly with AUC and advises against C0-guided adjustment). Tacrolimus and everolimus have trough windows.
This README grew with the app. §1 and §6 to §8 describe the engine and the adult MPA model, which came first; §12 to §14 say what is different for each later drug.
Is not, ever: a dose advisor. No dose optimization, no titration logic, no label or SmPC regimens, no wording that implies the app recommends anything. The user enters the regimen; the app shows consequences. This rule is absolute and is tested.
Intended user: clinical pharmacologist, TDM pharmacist or physician. Tone: clinical, plain-language, cautious, cited.
cd mpa-tdm
npm test # == run_all.js (111 MPA/engine tests) && test_tacrolimus.js (60) && test_everolimus.js (39) && test_pediatric.js (64) && test_report.js (9) && test_refresh.js (17) && test_audit150.js (22) && test_visits.js (6); all must stay green
npm run build # == node build.mjs → nephrotdm.html (the deliverable)
node tools/verify_model.mjs # model-ingestion gate: must pass (MPA, tacrolimus, everolimus and the two pediatric blocks)- Runtime: any modern desktop browser; open
nephrotdm.htmldirectly. Nothing to install. - Development: Node ≥ 16. No
node_modules, no dependencies of any kind. - Dev loop: edit
index.html/src/*→npm test→npm run build. Never test changes only in the built file.
- Never edit
nephrotdm.htmlby hand. It is generated. Editindex.htmlandsrc/*, thennpm run build. - Dependency-free and offline, forever. No CDNs, no remote fonts (the IBM Plex fonts are embedded as data URIs), no
fetchof external resources, no npm packages. The artifact works fromfile://. One exception, since 1.5.2: a single page-view image request to GoatCounter (src/visits.js), made only when the page runs onhttps://robterheine.github.io, carrying the page and the referring site and nothing entered by the user, honouring Do Not Track. A downloaded copy, localhost and every other host send nothing and show no error. It is an image, not a script, so no third-party code runs in a page where patient data are typed.tests/test_visits.jsholds this. Any further network use needs the owner's decision. - Time is in HOURS everywhere in this app: engine, ODE rates, grids, doses,
observations, charts, form fields, session JSON, report. Units follow the model: mg, mg/L and mg·h/L for MPA (the pediatric model takes mg of MMF and applies no
conversion), µg, µg/L and µg·h/L inside the tacrolimus and everolimus engines (doses are typed in mg and converted once, in
custom.doseToEngine). Do not port code from the complement-TDM app in the parent folder without converting units: that app uses DAYS in its engine. Day-to-hour constants are allowed in three places only: the recency time constant (365 days,bayes.js), calendar-day indexing of occasions (tacrolimus.js,mpaped.js), and the EC-MPS 24 h tiling check (model.js). Nothing else insrc/may contain/ 24or* 24;tests/test_audit150.js(H5) holds the list, so a new use has to be added there on purpose. - The version lives in exactly one place:
src/version.js.package.jsonmust match;build.mjswarns and the test suite fails if they drift. Bump both for every user-visible change. npm testafter every change, green before done. Any new assertion must be shown to fail first (see §9).- No dosing strategies, no preprogrammed regimens. The app never suggests, recommends, or optimizes a dose or interval.
- Every clinical number is traceable to a cited source and reproducible from
the session JSON.
7b. A green check is not evidence until it can go red. Show new tests failing
against pre-fix code before trusting them against the fix.
7c. Never put a step grid on a clinical number field. Use
step="any"+novalidateand validate in JS. - Escape all user-entered text before putting it into HTML (report, tables, tooltips). Never interpolate raw user input.
- Keep the main screen light. Long explanations belong in About, the drug's background dialog ("Mycophenolic acid background", "Tacrolimus background", ...), tooltips or collapsed folds, not on the main cards.
- Research use only. The disclaimer stays visible on screen and inside the printed report.
These are the behavioral rules for agents working here. They are adapted from
andrej-karpathy-skills (MIT)
and merged with this repo's conventions. CLAUDE.md in the parent repository carries the
same text (it is not part of the published repository). Where they overlap with the golden rules above
(e.g. §4.4 and rule 7b), the golden rules are the authoritative, more specific version.
Don't assume. Don't hide confusion. Surface tradeoffs.
- State assumptions explicitly before implementing. If uncertain, ask.
- If multiple interpretations exist, present them. Don't pick silently.
- If a simpler approach exists, say so. Push back when warranted.
- If something is unclear, stop, name what's confusing, ask.
Minimum code that solves the problem. Nothing speculative.
- No features beyond what was asked.
- No abstractions for single-use code.
- No "flexibility" or "configurability" that wasn't requested.
- No error handling for impossible scenarios.
- If it could be a third as long, rewrite it that short.
Touch only what you must. Clean up only your own mess.
- Don't "improve" adjacent code, comments, or formatting.
- Don't refactor things that aren't broken.
- Match existing style (here: ES5-ish
var, IIFE modules, oneECUnamespace, see §5), even where you'd do it differently. - If you notice unrelated dead code, mention it, but don't delete it unless asked.
- Remove imports/variables/functions that YOUR change made unused.
The test: every changed line should trace directly to the request.
For multi-step work, state a brief plan first, then execute step → verify → step:
1. [Step] -> verify: [check]
2. [Step] -> verify: [check]
Every new test assertion must be run against pre-fix code and shown to fail before it is trusted against the fix (this repo does this literally: sabotage the fix, re-run, confirm red, restore, re-run, confirm green).
Tradeoff note: this discipline biases toward caution over speed. For a truly trivial change (a typo, a copy fix), use judgment.
nephrotdm.html GENERATED single-file app, the deliverable. Never edit.
LICENSE, LICENSE-fonts.txt The project licence (MIT) and the SIL Open Font License text for the embedded IBM Plex fonts (the same text sits in a comment in the built page).
index.html Development entry page (loads src/app.css + src/*.js).
build.mjs Inlines app.css + the src modules into nephrotdm.html;
warns if package.json and src/version.js drift.
package.json Two scripts only: test, build. No dependencies.
src/
version.js ECU.VERSION, the single source of truth for the version.
tacrolimus.js ECU.model.drugs.tac: the tacrolimus spec and ALL its model code (indivParams,
closed-form simulate with per-dose F and ka, observation transform, assay
conversion, occasion bookkeeping, exposure). Reached from model.js/bayes.js
only through `spec.custom` hooks, so the MPA code path is untouched.
texts_tac.js Tacrolimus background / getting-started / About copy (no window numbers). Registers ECU.drugTexts.tac.
everolimus.js ECU.model.drugs.evr: the everolimus spec and ALL its model code (closed-form Erlang-5 + 3-compartment solution,
steady state, blood transform, exposure). Also reached only through `spec.custom` hooks. `spec.ui` carries its
presentation flags (and tacrolimus's): noun, weight, predDose, occasions, badge, chartTitle, shrinkEta, modelLine.
texts_evr.js Everolimus texts and result/report/explorer sentences; registers ECU.drugTexts.evr (ui.js reads the registry).
tacped.js ECU.model.drugs.tacped: pediatric tacrolimus: the liver engine with Erlang-3 absorption, weight scaling, per-dose formulation (KA, F), exact steady-state sum
(ssh, with a fallback near a pole), Simpson exposure grid, scope warnings. Reached only through `spec.custom`.
mpaped.js ECU.model.drugs.mpaped: pediatric MPA: two poles, Erlang-2 absorption, albumin and weight scaling, one occasion eta on F per sampled calendar day,
AUC = dose/CL exactly. `spec.ui.bloodCorrection: false` makes ui.js and report.js drop the haematocrit, corrected rows and trough window.
texts_tacped.js Pediatric tacrolimus texts; registers ECU.drugTexts.tacped.
texts_mpaped.js Pediatric MPA texts; registers ECU.drugTexts.mpaped.
fonts.css IBM Plex (Sans variable, Mono 500/600), Latin subset, embedded as data URIs (SIL OFL); the build inlines it before app.css.
report.js The one-page A4 report for every drug: ECU.report.build(ctx), pure (ui.js builds ctx from the run snapshot). Per-drug facts are in `spec.report` (lead tile, scope, window source, reading notes, short citation). Greyscale-safe, nothing under 12 px.
author_photo.js The author photo (inline WebP, ~39 KB base64) shown at the bottom of About; the same image as in complementtdm.
parallel.js Runs the independent MCMC chains of a fit on a pool of Web Workers built from the page's own inlined
scripts; falls back, with identical draws, to the in-process run. Used by every drug that declares `perChainSeeds`
(all but adult MPA; bayes.js `chainSeed`, `runChainTask`); adult MPA keeps its single-stream sampler. The worker
source list (`SOURCES`) must name every drug file. See docs/PERFORMANCE_AUDIT_V121.md.
model.js ECU.model: drug registry and the picker order (`DRUG_ORDER`), the adult MPA spec, ODE solver,
event loop, simulate(), aucFromConc(), intervalGrid(),
covariateFields(), installStubSpec()/restorePendingSpec().
bayes.js ECU.bayes: the engine: makeOfv() (prior + residual),
mapEstimate() (multi-restart Nelder–Mead, adult MPA) and mapBFGS() (drugs with
`spec.custom`), laplaceCov(),
runMCMC() (RW Metropolis, seeded RNG), runFit() (the main
entry: fit + AUC + trough + window probabilities + draws),
doseScan() (steady-state dose explorer), probAbove/Below/
Between().
chart.js ECU.chart: a dependency-free SVG concentration–time chart
(linear/log-y, band, observed points, no libraries).
diagnostics.js ECU.diagnostics: the fit-diagnostics panel for the modal that
opens via the “Fit diagnostics” button (enabled after a
forecast): observed-vs-predicted chart, sample-by-sample
residuals, posterior parameter ranges, shrinkage bars,
MCMC quality (residualRows/gofChart/summaryHint/panelHtml).
ui.js ECU.ui: the app shell: pending gating, model-driven covariate
rows, dosing/sampling entry, run flow, results grid, dose
explorer, report printing, session export/import, autosave,
modals (About / Getting started / the drug's background),
help popovers. Exposes state, buildRunInput(), validateRun(),
effectiveDoses(), windowBounds(), aboutHtml()… for tests.
app.css All styles, including the print stylesheet (§7).
tests/
harness.js Minimal xUnit: t(), assert(), eq(), near(), throws(),
rejects(), truthy(), falsy(), runAll() (async). Exit code
is wired: a failing suite fails the process.
test_visits.js The visit counter (6 tests): only on the published site, only the page and the referring site, nothing offline, Do Not Track, never throws.
test_audit150.js The findings of the 13 October 2026 adversarial review (22 tests): interval sanity, latest-sample haematocrit, the NaN convergence gate, session import and round trip,
the report snapshot, the explorer guard, Ctrl+P, the harness's own timeout and rejection handling, and the day/hour constant guard (rule 3).
test_refresh.js The visual refresh (17 tests): fonts, tap size, header and Session menu, one primary action, five drug cards, result tiles, workspace, stepper, phone, wiring of the pediatric files.
test_report.js The one-page report (9 tests): order, citations, per-drug tiles, escaping, safety notes, greyscale and type size, print setup.
test_pediatric.js The pediatric suite (64 tests): independent matrix-exponential oracle (`tools/nonmem_verify/ped/ped_oracle.mjs`), the section 4.4 reference numbers, NONMEM structure and
POSTHOC fixtures, KA near a pole, occasions, scope, texts, the screen and report behaviour, regression records.
mpaped_regression.json, tacped_regression.json, nonmem_ped_* its fixtures (re-record only for an intended change; cases in `tools/ped_regression_cases.mjs`).
test_everolimus.js The everolimus suite (39 tests): matrix-exponential and RK4 oracles, the §3.5 reference table, Eq. 3, NONMEM fixtures
(structure and POSTHOC), worker pool, wiring, copy rules, the UI-text snapshot of MPA and tacrolimus, regression record.
evr_regression.json, ui_text_snapshot.json, nonmem_evr_struct.*, nonmem_evr_posthoc.json its fixtures (re-record only for an intended change).
test_tacrolimus.js The tacrolimus suite (60 tests, incl. an independent RK4 oracle, the Størset
anchors, the sabotage-verified invariants and V16, the MPA regression against
mpa_v111_regression.json).
run_all.js The MPA/engine suite (111 tests): pending gate, engine P0/P1
guards under the stub spec, clinical metadata, UI copy,
version sync. Edit here to add tests.
golden.json Golden reference values for the de Winter 2008 implementation
(analytic mass balance and published anchors).
tools/
nonmem_verify/ NONMEM cross-check (structural model + POSTHOC vs the app's MAP). `run.sh`
runs it end to end with your NONMEM; results in docs/NONMEM_CROSSCHECK.md;
`tests/nonmem_golden.json` carries NONMEM's numbers into `npm test`.
record_tac_regression.mjs Re-records tests/tac_v121_regression.json (bit-identity baseline for optimisations).
calibrate_storset.mjs Tacrolimus simulation–recovery study (9 cells; results in docs/CALIBRATION_RESULTS_STORSET.md).
calibrate_evr.mjs Everolimus simulation–recovery study (12 cells; docs/CALIBRATION_RESULTS_EVR.md, tables by assemble_calibration_evr.mjs).
audit_fields.js Browser audit: does the text in every visible field fit its box? `window.auditFields()` after loading the built page; run it at 320 to 1920 px, for every drug and schedule mode (it found the clipped date and select fields of 4 Oct).
record_evr_regression.mjs, record_ui_snapshot.mjs Re-record tests/evr_regression.json; record the MPA/tacrolimus UI text (only BEFORE a deliberate change).
record_ped_regression.mjs, ped_regression_cases.mjs Re-record tests/mpaped_regression.json and tests/tacped_regression.json (the cases live in one shared module).
nonmem_verify/ped/ Pediatric NONMEM check: `run_ped.sh` (structure; `POSTHOC=1` and `BAYES=1` for the later parts), the independent oracle, the authors' verbatim streams.
calibrate_ped*.mjs The independent calibration and benchmark of the pediatric models (written by the statistician seat; results in docs/CALIBRATION_RESULTS_PEDIATRIC.md).
evr_prototype/ Matrix-exponential and RK4 oracles and the prototype closed form for everolimus (used by the tests).
nonmem_verify/evr/ Everolimus NONMEM check: `bash tools/nonmem_verify/evr/run_evr.sh` (structure + POSTHOC); export_golden_evr.mjs writes the test fixtures.
verify_model.mjs Model-ingestion gate (MPA, tacrolimus, everolimus and the two pediatric blocks). All must pass.
docs/
RELEASE_NOTES_V110/V120/V140/V150.md What each release changed, with its evidence and open items.
HANDOFF_EVEROLIMUS_M3.md, HANDOFF_PEDIATRIC.md The plan, decisions and source readings of the two later model additions.
VERIFICATION_TEAM.md, VERIFICATION_PEDIATRIC.md Review gates and verdicts; REVIEW_PEDIATRIC_*.md are the three independent reviews.
CALIBRATION_RESULTS*.md, NONMEM_CROSSCHECK*.md Simulation-recovery studies and the NONMEM comparisons, per drug.
MODEL_ANALYSIS_*.md, IMPLEMENTATION_PLAN_*.md, SOURCE_ANALYSIS_*.md Transcription records, plans and source readings.
METHODS_AUDIT_V101.md, ADVERSARIAL_*.md, PERFORMANCE_AUDIT_V121.md The audits (all P0/P1 items fixed; P3-2 in-table dose editing is open).
Module pattern (match it): every src/*.js is an IIFE that attaches to the single
global namespace root.ECU and ends with
})(typeof window !== 'undefined' ? window : globalThis); so the same files run in
the browser and under Node for tests. ES5 style (var, function declarations),
no build transpilation, no modules, no bundler.
Units of the adult MPA engine: hours, mg, mg/L, mg·h/L (rule 3). The tacrolimus and everolimus engines work in µg and µg/L; §12 to §14 say so.
- OFV(η) =
η' Ω⁻¹ η(prior) + Gaussian residual likelihood with log-transformed variance. Censored (< LLOQ) samples are not supported (owner decision, v1.1): there is no LLOQ input, no per-sample box and no M3 likelihood, andrunFitrefuses a sample carryinglloqrather than dropping it or fitting a value (a zero biases AUC by ~27%, the v0.2.4 audit defect S1/C1). Users omit such samples; an old session file's censored samples are left out on import with a message. There is likewise no assay field and no error-multiplier input (the model has no assay covariate). - MAP: multi-restart Nelder–Mead over η. Posterior: random-walk Metropolis
started at MAP, proposal scaled from a Laplace Hessian, 40% burn-in + thinning,
seeded
mulberry32RNG → results are reproducible run-to-run. - AUC₀–12h (app-wide target): trapezoid integration of each posterior curve over
[t_last dose, t_last dose + τ]on an hour grid (24 intervals), then normalized per draw: AUC₁₂ = (AUC₀₋ₓ/x)·12 (identity at x=12). Because the transform is positive-linear, quantiles map exactly and window probabilities computed on the normalized chain equal the raw chain tested against bounds × 12/x, so the forecast targets the AUC₀–12h window whatever interval the patient uses.aucRaw/aucChainkeep the as-simulated AUC₀₋ₓ; the UI shows the provenance ("from AUC₀–24h … × 12/24"). Summary is the posterior median with a 5–95% interval. - Trough = C(t_last + τ). Never
min(curve). No target, informational only. - Window probabilities: strict P(lo < AUC < hi) plus P(below lo) and P(above hi); both bounds are real. Defaults 30–60 mg·h/L (§7) and are editable clinical input.
- Single-dose guard: in full-schedule mode with only one dose, the app labels the AUC as single dose from zero, not steady state (badge, warning, report note) instead of presenting it as an SS value (audit defect S2/C2, fixed in v0.2.4).
- Dose explorer (
doseScan): replays a full maintenance regimen (ssNDosesdoses, default 10) at the candidate amount and the user-chosen interval (12 or 24 h) to true steady state, reusing this patient's posterior draws. It does not refit and does not pick doses. All AUCs are normalized to a 12-hour equivalent, AUC₁₂ = (AUC₀₋ₓ/x)·12, applied per posterior draw, the same contract as the main forecast (see the AUC bullet above).aucRawcarries the as-simulated AUC₀₋ₓ for display; the trough stays C(x), never normalized. Output contract: expected AUC₁₂ + 5–95% interval, the three window probabilities, predicted trough. - Steady-state input mode: the UI accepts "dose + interval" directly and
reconstructs the implied schedule (
effectiveDoses()); full-schedule mode accepts actual dose events. Both feed the samerunFit.
The generic machinery is also tested through installStubSpec() (a tiny 1-compartment
oral model inside model.js): tests unlock the stub, prove the machinery, then
restorePendingSpec(). The stub never ships enabled; the real MPA spec is live.
Other drugs. A drug with spec.custom supplies its own parameters, simulation, ingestion and exposure hooks. It takes the BFGS MAP (Nelder–Mead is unreliable beyond about
8 dimensions) and reports the steady state of the last regimen on a typical day, not the history that was typed (§12 to §14). One thing is shared by every drug: makeOfv floors
the residual variance at 1e-6. NONMEM's likelihood has no such floor; it matters only for predictions below about 2 mg/L (MPA) or 5 ng/L (tacrolimus). The owner decided to leave it.
- Therapeutic window default 30–60 mg·h/L for kidney transplant, MMF + CNI ± steroids, adult & pediatric (Bergan et al., Ther Drug Monit 2021;43:150–197, grade B, II).
- The on-screen window hint is deliberately a short pointer ("See “Mycophenolic acid background” for reference values and best practices for TDM", golden rule 9; the ≤120-char limit is test-enforced). The full per-indication table lives in the "Mycophenolic acid background" modal: liver 30–60; heart >36; lung none established; adult HSCT AUC₀–₂₄h >30 (different interval); proliferative lupus nephritis ≈50 (prefer 45–60; Wuttiputhanun et al., Lupus Sci Med 2024;11:e001093); childhood nephrotic syndrome >45–50 (Querfeld & Weber, Pediatr Nephrol 2018;33(12):2253–2265); other autoimmune use has no validated target (local practice). The ⓘ help on the window fields carries the same list in short form. Transplant targets apply mainly to the first posttransplant year and are not validated for EC-MPS.
- Sampling guidance: MMF ≥2 samples (typical LSS 20 min, 1 h, 3 h); EC-MPS 3–4 samples (1.5 h, 2 h, 4 h ± 6 h); trough-only discouraged. Shown at sample entry and in Getting started.
- Formulation is the only covariate and is printed in the report. There is no assay field: the model has no assay covariate, so recording one implied an effect it does not have (immunoassays overestimate MPA 15–37% vs LC-MS/MS; the About text says so).
- The therapeutic window is clinical input, prefilled from the spec, editable, with its source shown. The app takes no position on the window.
When a clinical number changes, update the spec in src/model.js, keep the citation
with it, and keep the tests that lock it (tests/run_all.js, "Consensus clinical
metadata" section) in step. They assert the hint cites Bergan and covers every
indication, so an accidental revert to kidney-only text fails the suite.
The MPA spec in src/model.js now carries the de Winter 2008 model (final model 4):
CL 16, Q 22, V1 40, V2 518 (L/h, L), ka 4.1 (MMF) / 3.0 (EC-MPS) per hour, tlag 0.30 h
(MMF) or the 51/32/17% morning mixture (0.95/1.88/4.83 h) + 9.04 h evening lag
(EC-MPS), log-scale residual σ 0.39, no allometry, no covariates except formulation.
The full analysis, scrutiny (S1–S12) and validation matrix live in
docs/MODEL_ANALYSIS_DEWINTER_2008.md and docs/IMPLEMENTATION_PLAN_DEWINTER_2008.md.
The ingestion procedure (kept for the next model):
- Fill the spec fields in
src/model.js(structure, THETA, ETA + iiv, SIGMA, FORMS, covariates). Convert any day-based parameters to hours at this boundary only (rule 3). - Set
pending: falseand writeinfo/article/assumptions(the model card reads them). - Extend
covariateFields(); the Patient card generates covariate inputs from it. npm testgreen; golden values intotests/golden.json.node tools/verify_model.mjs(strict) must pass.npm run build; bump the version.
For a new drug, do not edit the engine. Use the spec.custom route that everolimus and the pediatric drugs took: copy src/everolimus.js and src/texts_evr.js
as the template, then register the new files in build.mjs (FILES), index.html (script tags and the hidden #pt-drug select), src/parallel.js (SOURCES),
tools/verify_model.mjs and DRUG_ORDER in src/model.js. Add a card (spec.card), spec.report and the texts; the picker, the UI and the report read the spec.
The handoffs of §13 and §14 list the decisions each addition needed.
Numerical validation of the adult MPA model (see the docs): mass-balance golden anchors (MMF 1000 mg → AUC₁₂ = 739/16 ≈ 46.2), the analytic IV bi-exponential, the EC-MPS 24h-periodicity invariant (morning + evening windows = 2 × dose/CL), SS depth 30 doses (terminal t½ ≈ 40 h), and the V10 simulation–recovery calibration with the S1/Ω-approximation robustness arms.
- The suites are
tests/run_all.js(adult MPA and the engine) andtest_tacrolimus.js,test_everolimus.js,test_pediatric.js,test_report.js,test_refresh.js,test_audit150.jsandtest_visits.js, all ontests/harness.js(asynct()cases;await runAll()is wired in each runner; the exit code reflects failures). Failures go to stderr, so read both streams. The harness gives every test a timeout (300 s,TEST_TIMEOUT_MSto change it) and fails the run on an unhandled promise rejection. - What the regression records are, and are not.
V16,V16band the pediatric records were recorded from the code at the time, so they prove that a later change did not move the numbers, not that the numbers were right at recording. The independent evidence is the NONMEM cross-checks, the oracles and the exact anchors (S2/S3). A sampler-only defect that was already present when a record was made would be pinned as correct; the NONMEM BAYES comparison is what covers that. - Every assertion must be able to fail. Before trusting a new test, break the thing it guards and watch it go red, then restore and watch it go green. Tests that cannot fail are deleted or fixed.
- Engine tests use
installStubSpec()/restorePendingSpec()and never leave the stub installed. - UI-copy tests load
src/ui.jsunder Node (it no-ops DOM binding without adocument) and assert on the exportedaboutHtml()/gettingStartedBodyHtml()/backgroundHtml()strings; copy changes must keep these in step. - The version-sync test compares
src/version.jswithpackage.json; runnpm testafter any bump.
- Print: the report is
#reportSheet;@media printhides everything else. Any new visual work must not break this (verify with Print → preview). - Modals are
div.modal-backdrop[hidden]+.modal; the close-ids list inui.jsmust include every new modal; Escape and backdrop-click close them. - Help popovers:
button.info[data-help="key"]+ theHELPmap inui.js. - Session export/import and autosave serialize
ECU.ui.state; new fields must round-trip (add a test). - Time inputs are civil datetime-local strings converted with
dtLocalToHours()/hoursToDtLocal(), timezone-naive by design. - The parent repo contains a sibling complement-TDM app (mAbs) with a similar shape but a days-based engine and a different CSS. Treat it as inspiration for patterns only, never as copy-paste source.
-
Adversarial review of 13 October. Fixed: C1, C2, H1 to H6 and most of the Medium and Low items, each with a test that failed on 1.5.0 first. Not changed, with reasons, in
docs/ADVERSARIAL_FIXES_V151.md: the multi-start MAP (below), the roughly 50 source-text tests, the confirmation on a single row delete, and the dose-advice copy bans. -
Pediatric drugs. Three independent seats reviewed them and each gave "pass with comments" (
docs/REVIEW_PEDIATRIC_*.md,docs/VERIFICATION_PEDIATRIC.md). The wording they asked for was applied afterwards and has not been re-reviewed. Still open: the tacrolimus 3-point NRMSE in the benchmark replication (16.3 % simulated against 7.8 % published; accepted as a documented point, to be put to the authors); the owner's check of the Brunet 2019 trough ranges and of Schijvens 2020; a print check of the report in Safari itself (1.5.1 checked A4 and Letter in headless Chrome and scales a tall report to fit one page; before that, one report configuration was 16 px over the 1103 px safety margin); a multi-start MAP (single-start can end in a worse mode; a 1.5.1 candidate); two-line MPA card names at 320 px and the fold position of the first input with five cards (designer and owner decisions). Seedocs/RELEASE_NOTES_V150.md, "Open items". -
Everolimus. No named human has reviewed it (
docs/VERIFICATION_TEAM.md, addendum written). The page is far above the old ~350 KB budget (the owner accepted the growth for the fonts). No interaction field, no transplant-date field and no AUC window, by owner decision. -
Tacrolimus (adult). The standard kidney window is shipped (trough 4–12, AUC 150–210; the AUC upper bound is the app's choice,
docs/SOURCE_ANALYSIS_BRUNET_2019.md). There is no transplant-date field (the first-weeks bias is stated in the text, not detected); once-daily and other schedules are refused, not modelled. Seedocs/ADVERSARIAL_AUDIT_V121.md.
- Author queries (S1/diagonal-Ω): full Ω covariance and the ω² convention from the
de Winter authors: drafted (the drafts,
docs/AUTHOR_QUERY*.md, are kept private and are not in this repository); the robustness arms measured the impact as AUC-benign for rich sampling; with sparse data (trough only) the choice moves the interval width ≈10 % and P(window) a few pp; stated in the model card. The paper reports IIV as % only (Table III); the conversion is our reading. EC-MPS window anchoring (S12): shipped in v1.0.1:M.aucAnchorauto-anchors the AUC window at the most recent morning dose for evening-ending EC-MPS schedules (fit unchanged, only the derived window moves; UI/report/diagnostics carry the note).- P3-2: dose/observation tables are delete-only; in-place editing would help.
- ~~Browser runtime ~30–60 s per fit~~: resolved in v1.1.0: closed-form propagation and exact steady state, ≈0.5 s per fit; no worker thread needed.
- EC-MPS convergence: 5–15 % of EC-MPS fits do not pass the R̂/ESS check (slow mixing of the discrete absorption subgroup; more iterations do not fix it). The app flags them. A sampler change (joint η/membership moves) is the remedy.
- MAP in the diagnostics dialog is a single-start optimum and can be a local minimum with sparse
data (NONMEM's POSTHOC behaves the same;
docs/NONMEM_CROSSCHECK.md). Reported AUC/trough/probabilities use the posterior and are unaffected. Multi-start MAP and a marginal EC-MPS subgroup choice would change the displayed individual predictions. Not done, owner's call. - Verification gate (
docs/VERIFICATION_TEAM.md): the verdicts for the adult drugs and everolimus arepending; no independent human review has signed. The pediatric verdicts are from agent seats, not named people. - Step grids remain on
pt-wt,pt-age,ss-interval(golden rule 7c; not on the audit's list).
- Units: engine in µg and µg/L (doses are entered in mg; the single mg→µg conversion is
custom.doseToEngine; a test forbids* 1000and the immunoassay constants anywhere else). Time is still in hours (golden rule 3). - Model: plasma PK (2-compartment, ka, lag 0.41 h), scaled to fat-free mass; whole blood = Cp·(1+Hct·418/(Cp+3.8)); CYP3A5 and prednisolone act on CL and F; Ω is a correlated 3×3 block (CL, V1, Q) plus one κ on F and, only for days with a sample ≤ 4 h after a dose, one κ on ka, per sampled day (12 most recent). The first-day bioavailability effect and any genotype mixture are not modelled (owner decisions D2, D4).
- What is reported: steady state of the last regimen on a typical day (κ = 0), also when a finite history was typed; AUC₀–₁₂ₕ and trough, actual (haematocrit of the latest sample) and corrected to 0.35. An unset window gives intervals without probabilities.
- Assay: LC-MS/MS or Abbott CMIA (Størset 2014 Eq. 1). Results are shown on the chosen scale.
- MAP/sampler: BFGS MAP (Nelder–Mead is unreliable beyond ~8 dimensions) and a dimension-scaled random-walk step; MPA keeps its validated settings.
fit.shrinkis shrinkage, Var(post)/ω² (1 = the samples left the population variance in place; fixed in v1.2.1: until v1.2.0 it held information gained, 1 − Var/ω², and the MPA bars and sparse-data note read it backwards;docs/METHODS_AUDIT_V101.md, ST4 addendum). Tacrolimus still shows and tests information gained (1 − fit.shrink), deliberately: its bars are red when low and its note fires below 0.3.
- Model: Model 3 of Zwart 2021, used exactly as coded in the supplement (mg and mg/L there; µg and µg/L in the engine, binding constants ×1000). Five equal absorption stages,
a liver compartment with flow-limited extraction (plasma flow QH·(1 − Ht), so haematocrit is a PK covariate), two-compartment plasma disposition, saturable red-cell binding.
Three etas (CLINT, V3, FU), diagonal Ω, log-scale residual (
SIGMA.LOG), no occasion layer. Prednisolone at 20 mg/day or more multiplies CLINT by 1.44 (a required two-way choice, no default). - Closed form:
src/everolimus.jssolves the three-pole disposition (cubic, trigonometric) and convolves it with the Erlang-5 input; steady state is exact (geometric sum). The small-x and λ > k ranges use a tail series with no division by (k − λ). If the cubic does not give three real negative roots the run is refused (poles()returns null), never approximated. - What is reported: steady state of the last regimen, AUC₀–₁₂ₕ and trough, actual (haematocrit of the latest sample) and corrected to 0.38 (Eq. 3: the same plasma curve read at 0.38).
No AUC window ships; trough sets
evr-cni(3–8, default) andevr-nocni(6–10). An unset window gives intervals without probabilities. A run without a sample is allowed (population forecast). - Haematocrit: one value for the PK path (the latest sample's), each sample's own for the blood reading. NONMEM uses the Ht of the record at the end of each interval; the measured cost is mean 1.0 %, max 6.2 % in
earlier-sample predictions (test:
NONMEM: the everolimus structural model ...). - Assay: LC-MS/MS only, one fixed line, no assay field. Scope: 10–14 h interval, 0.25–5 mg, 0.5–60 µg/L, haematocrit 0.10–0.65 L/L; checks in
inputProblems. - UI wiring:
ui.jsreadsspec.uiflags andECU.drugTexts[id]; a drug without an entry gets neutral text. Never hand-edit a drug's wording into a shared string: the snapshot test (tests/ui_text_snapshot.json) holds MPA's and tacrolimus's text and fails on any change. - Accuracy claims in About come from Zwart 2021 Table 2 (Model 3), checked against the PDF; "about two thirds within ±30 %" is Table 2's 65–69 %.
- Verification record:
docs/RELEASE_NOTES_V140.md(checks, sabotage list) anddocs/CALIBRATION_RESULTS_EVR.md.
- Registration:
src/mpaped.jsandsrc/tacped.jsarespec.customspecs like everolimus; the card order isDRUG_ORDERinmodel.js; nothing else inui.js,report.jsordiagnostics.jsnames a drug (a test checks it). Flags on the spec:ui.bloodCorrection(false for MPA),doseForms(per-dose formulation),ui.windowOn: 'corrected'(tacrolimus pediatric),ui.doseNoun,scopeandcustom.scopeWarnings(the range the model was built on: a warning, never a refusal). - MPA: dose in mg of MMF, concentration mg/L of MPA, no 0.739 factor (test MI3: 600 mg gives AUC 58.7, not 43.4). Albumin normalised by 34 g/L and the exponent -2.49 (the ESM text and Eq. 1 say 35 and -2.5; run 57 and Table 2 agree with 34). Steady-state AUC = dose x exp(kappa)/CL exactly. Occasion = calendar day, a pre-dose sample belongs to the previous dose's day, up to 10 sampled days.
- Tacrolimus: the formulation is a property of the dose (
d.form), carried byssHistory,prepare,runFit(steady-state spec) anddoseScan. NONMEM applies the record's KA to doses still in transit; the app gives each dose its own (difference at most 1.3e-3 at 12-hourly dosing). The corrected value is the same plasma curve read at haematocrit 0.35; the windows are judged on it. - Do not use NONMEM
SS=1with ADVAN6 as the tacrolimus reference for fast absorption (4 % off at KA about 60 /h); the stored fixtures use explicit 80-dose trains. Run NONMEM BAYES jobs one at a time (two in one directory overwrite each other's work files) and do not mark every parameter FIX in a BAYES stream (no estimation step). - Verification tools:
tools/nonmem_verify/ped/run_ped.sh(structure;POSTHOC=1,BAYES=1for the later parts),tools/calibrate_ped*.mjs(the independent calibration and benchmark, written by the statistician seat),tools/record_ped_regression.mjs. - Which value the window is judged on. The tacrolimus targets are whole-blood values that the sources take to refer to haematocrit 0.35, so pediatric tacrolimus judges the corrected
value (
ui.windowOn: 'corrected'). The tile, the chips, the printout and the saved session say which value and which window set (state.winSet,windowNoteOf); typed bounds read "your own bounds". Pediatric MPA has one set (30 to 60) and no corrected value. - Conventions. The reported AUC is the typical day (occasion effects zero), and unsampled dosing days carry an effect of zero. The true AUC of a single day scatters about it by ×/÷ 1.2 to 1.3 (one SD). The one-haematocrit PK path costs a few per cent on average (up to about 20 % for some earlier samples) when the haematocrit differs by more than about 0.05 between samples. Warnings, never refusals: weight outside the data (MPA 12.9 to 79.9 kg, tacrolimus 9.1 to 78 kg), albumin outside 24 to 42 g/L, haematocrit outside 0.15 to 0.60. Refusals: weight outside 3 to 200 kg, albumin outside 5 to 50 g/L, haematocrit outside 0.10 to 0.70.
- Not in the app, by decision: the starting-dose table of Heida 2024, mg/m² guidance, an mg/kg dose check, an age field, a transplant-date field, a "sampled-day" line (D3), the app's own simulation figures in its text, a rename of "Run forecast".