marmar-context / src / domain / checks / kidney-dose.ts
kidney-dose.ts
Raw
import { implausibleResult } from '../labs/targets.js';
import type { CheckLab } from './engine.js';
import type { EngineHooks, KidneyDoseLine } from './options.js';
import { SPECIFIC_REFERENCES } from './sources.js';

/**
 * CHK-11 (kidneyDoseLimits): metformin's kidney-adjusted maximum from the Glucophage 500 mg UK SmPC (Merck) 4.2 and 4.3,
 * read 30 October 2026 (revised November 2025). The SmPC gives GFR in mL/min; the engine compares eGFR, as its lab rules do.
 */
export const METFORMIN_KIDNEY_BANDS: ReadonlyArray<{ band: string; from: number; below: number | null; maxDailyMg: number | null }> = [
  { band: 'under 30', from: -Infinity, below: 30, maxDailyMg: null },
  { band: '30-44', from: 30, below: 45, maxDailyMg: 1000 },
  { band: '45-59', from: 45, below: 60, maxDailyMg: 2000 },
  // 60 and over: the adult maximum (3000 mg) applies, which the usual dose check already uses.
];

const REFERENCE = SPECIFIC_REFERENCES['kidney-dose-metformin']![0];
/** A result older than this isn't used, as the lab rules don't use it (engine STALE_RESULT_DAYS). */
const STALE_DAYS = 90;

const dateOf = (iso: string) => new Date(`${iso.slice(0, 10)}T00:00:00Z`).toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric', timeZone: 'UTC' });

/** The band a result is in; for a value printed as a bound, the band only when every value it allows is in it. */
function bandOf(lab: Pick<CheckLab, 'value' | 'comparator'>) {
  const low = lab.comparator === '<' || lab.comparator === '<=' ? -Infinity : lab.value;
  const high = lab.comparator === '>' || lab.comparator === '>=' ? Infinity : lab.value;
  if (low >= 60) return 'none' as const;
  return METFORMIN_KIDNEY_BANDS.find((band) => low >= band.from
    && (band.below === null || high < band.below || (high === band.below && lab.comparator === '<'))) ?? ('unclear' as const);
}

/** The hook the engine calls for each medicine's dose line (EngineHooks.kidneyDose). */
export function kidneyDoseHook(labs: readonly CheckLab[], today: string, calculatedLab: object | null): NonNullable<EngineHooks['kidneyDose']> {
  const egfr = [...labs].filter((lab) => lab.code === 'egfr').sort((x, y) => x.observedAt.localeCompare(y.observedAt)).pop();
  return ({ key, total }) => {
    if (key !== 'metformin' || !egfr) return null;
    const ageDays = (Date.parse(`${today}T00:00:00Z`) - Date.parse(egfr.observedAt)) / 86_400_000;
    if (ageDays > STALE_DAYS || implausibleResult('egfr', egfr.value)) return null;
    const band = bandOf(egfr);
    if (band === 'none') return null;
    const printed = `${egfr.comparator ?? ''}${egfr.value}`;
    const latest = `latest eGFR ${printed}${calculatedLab === egfr ? ', calculated from creatinine' : ''}, ${dateOf(egfr.observedAt)}`;
    const base = { ingredient: 'metformin', egfr: egfr.value, egfr_comparator: egfr.comparator ?? null, egfr_observed_at: egfr.observedAt,
      egfr_calculated: calculatedLab === egfr, daily_mg: total, source_id: REFERENCE.sourceId, section: REFERENCE.section };
    if (band === 'unclear') {
      return { status: 'check_with_pharmacist', contraindicated: false, kidney_limit: { ...base, band: 'unclear', max_daily_mg: null },
        plain: `The latest eGFR was printed as "${printed}", so the label's kidney-adjusted maximum for metformin couldn't be worked out.` } satisfies KidneyDoseLine;
    }
    const limit = { ...base, band: band.band, max_daily_mg: band.maxDailyMg };
    if (band.maxDailyMg === null) {
      return { status: 'above_limit', contraindicated: true, kidney_limit: limit,
        plain: `The metformin label lists metformin as contraindicated below an eGFR of 30 (${latest}).` };
    }
    if (total === null) return null;
    const max = band.maxDailyMg.toLocaleString('en-GB');
    if (total > band.maxDailyMg) {
      return { status: 'above_limit', contraindicated: false, kidney_limit: limit,
        plain: `About ${Math.round(total)} mg of metformin a day is more than the label's ${max} mg maximum for an eGFR of ${band.band.replace('-', ' to ')} (${latest}).` };
    }
    return { status: 'within_usual_range', contraindicated: false, kidney_limit: limit,
      plain: `The daily amount entered does not exceed the label's ${max} mg maximum for an eGFR of ${band.band.replace('-', ' to ')} (${latest}). `
        + 'This does not confirm the right dose to take.' };
  };
}