pediatric-ai-scribe-v3/shared/clinical/bmi.test.ts
Daniel 7371cbf13a feat(client): port BMI + Vitals + Resus Meds + Equipment calculators
Nine of the ten Calculator pills now run in React. Only BP Percentile
(Rosner quantile splines, ~3,000 hand-transcribed coefficients across
6 LMS arrays and 4 spline matrices) remains legacy-linked; that port
deserves its own dedicated session with extra care.

shared/clinical/bmi.ts — CDC 2000 BMI-for-age
  • bmiLMS table ported byte-for-byte from calculators.js:739
    (74 LMS triples = 37 age points × 2 sexes, 24-240 months).
  • normalCDF (Abramowitz & Stegun), calcBmiPercentile, classifyBMI
    (including the %-of-95th severe-obesity split), and a top-level
    computeBmi helper — all verbatim translations of the vanilla math.
  • classification labels preserved exactly so existing e2e screenshots
    or reporting continue to read the same text ('Class 2 Severe
    Obesity', 'Healthy Weight', etc.).

shared/clinical/bmi.test.ts — 12 captured vectors covering:
  both sexes, edges (2y + 20y), interpolated-between-keys (13m),
  each classification cliff (underweight / healthy / overweight /
  obese / severe class 2 / severe class 3), and the age-clamping
  branches (<24 mo and >240 mo). All 12 pass at 10-decimal precision
  (percentile to 6 places since the vanilla rounds to 2).

scripts/capture-calc-vectors.js — BMI section added
  Same pattern as bilirubin / Fenton: the vanilla data + math are
  inlined verbatim, the script runs 12 chosen cases, and writes to
  e2e/fixtures/calc-vectors.json. Re-run after any upstream change.

client/src/pages/CalculatorPanels.tsx (new)
  • BmiPanel — age/sex/weight/height inputs, calls computeBmi,
    renders color-coded classification badge with BMI, percentile,
    Z, and % of 95th when percentile ≥85.
  • VitalsPanel — 8-band age selector (premie → >12 yr).
    VITALS_DATA ported verbatim from calculators.js:1703-1831 with
    every HR/RR/SBP/DBP/temp/weight/SpO₂ range and clinical notes
    preserved entry-for-entry.
  • ResusPanel — weight input drives all 14 drugs (Adenosine,
    Amiodarone, Atropine, CaCl, Ca-gluconate, Dextrose, Epi,
    Hydrocortisone, Insulin, Lidocaine, Mg, Naloxone, NaHCO₃) with
    calc() closures ported verbatim from RESUS_MEDS lines 1873-2050.
    Category colors + labels preserved.
  • EquipmentPanel — 9-band age selector (premie → 16+ yr).
    EQUIP_DATA ported verbatim from calculators.js:2173-2228 with
    12 equipment sizes per band (BVM, NPA, OPA, blade, ETT, LMA,
    Glidescope, IV, CVL, NGT, chest tube, Foley).

client/src/pages/Calculators.tsx
  Dispatch wires bmi → BmiPanel, vitals → VitalsPanel, resus →
  ResusPanel, equipment → EquipmentPanel. PILLS flags all four as
  ported: true. Only bp remains on LegacyPanel.

Backend tsc + client tsc + vite build + 122/122 vitest (19 calc +
70 bili + 21 fenton/neonatal + 12 bmi) all green.
2026-04-24 02:08:05 +02:00

39 lines
1.6 KiB
TypeScript

// ============================================================
// BMI-FOR-AGE parity tests.
// ============================================================
import { describe, expect, it } from 'vitest';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { computeBmi } from './bmi';
import type { Sex } from './fenton';
interface BmiCase {
name: string;
inputs: { weightKg: number; heightCm: number; ageMonths: number; sex: Sex };
output: {
bmi: number; z: number; percentile: number;
L: number; M: number; S: number;
classification: { label: string; pctOf95: number; bmi95: number };
};
}
interface Vectors { bmi: BmiCase[] }
const vectors: Vectors = JSON.parse(
fs.readFileSync(path.join(__dirname, '..', '..', 'e2e', 'fixtures', 'calc-vectors.json'), 'utf8'),
);
describe('CDC 2000 BMI-for-age — parity with vanilla calculators.js', () => {
it.each(vectors.bmi)('$name', ({ inputs, output }) => {
const actual = computeBmi(inputs.weightKg, inputs.heightCm, inputs.ageMonths, inputs.sex);
expect(actual.bmi).toBeCloseTo(output.bmi, 10);
expect(actual.z).toBeCloseTo(output.z, 10);
expect(actual.percentile).toBeCloseTo(output.percentile, 6);
expect(actual.L).toBeCloseTo(output.L, 10);
expect(actual.M).toBeCloseTo(output.M, 10);
expect(actual.S).toBeCloseTo(output.S, 10);
expect(actual.classification.label).toBe(output.classification.label);
expect(actual.classification.pctOf95).toBeCloseTo(output.classification.pctOf95, 6);
expect(actual.classification.bmi95).toBeCloseTo(output.classification.bmi95, 10);
});
});