marmar-context / src / domain / labs / targets.ts
targets.ts
Raw
// Personal targets the check engine trusts (H3). A target outside these ranges
// is almost always a typing slip, 30 for 3.0, and would silence alerts: the
// targets API refuses it, and the check ignores one saved before this rule
// (and says so). Ranges are in the test's catalogue unit. Signed off with the
// rules on 15 October 2026, unchanged since the review pack (src/domain/checks/signoff.ts).

import { findLabTest, normalizeLabValue } from './catalog.js';

export interface TargetRange { min: number; max: number }

export const PLAUSIBLE_TARGETS: Readonly<Record<string, { low?: TargetRange; high?: TargetRange }>> = {
  inr: { low: { min: 1.5, max: 3.5 }, high: { min: 2, max: 4.5 } },
};

/**
 * Results the check engine will act on (N16). One outside these limits is
 * almost always a typing slip (a potassium of 55 for 5.5) and is named as not
 * looking right instead of raising an alarm. In the catalogue unit. Signed
 * off with the rules on 15 October 2026, unchanged since the review pack.
 */
export const PLAUSIBLE_RESULTS: Readonly<Record<string, TargetRange>> = {
  potassium: { min: 1.5, max: 10 },
  inr: { min: 0.5, max: 20 },
  egfr: { min: 1, max: 200 },
};

/** Whether a result is outside the limits the check engine trusts. */
export function implausibleResult(code: string, value: number): boolean {
  const range = PLAUSIBLE_RESULTS[code];
  return Boolean(range && (value < range.min || value > range.max));
}

export type TargetProblem =
  | { kind: 'out_of_range'; field: 'low' | 'high'; min: number; max: number }
  | { kind: 'low_above_high'; field: 'low' };

/** Why a personal target can't be trusted, or null when it can (or the test has no ranges set). */
export function targetProblem(code: string, target: { low: number | null; high: number | null; unit?: string | null }): TargetProblem | null {
  const ranges = PLAUSIBLE_TARGETS[code];
  if (!ranges) return null;
  const inCatalogueUnit = (value: number) => normalizeLabValue(code, value, target.unit ?? null)?.value ?? value;
  for (const field of ['low', 'high'] as const) {
    const value = target[field];
    const range = ranges[field];
    if (value === null || value === undefined || !range) continue;
    const comparable = inCatalogueUnit(value);
    if (comparable < range.min || comparable > range.max) return { kind: 'out_of_range', field, min: range.min, max: range.max };
  }
  if (target.low !== null && target.high !== null && target.low > target.high) return { kind: 'low_above_high', field: 'low' };
  return null;
}

/** "The INR target high must be between 2 and 4.5." */
export function targetProblemMessage(code: string, problem: TargetProblem): string {
  const name = findLabTest(code)?.name ?? code;
  return problem.kind === 'out_of_range'
    ? `The ${name} target ${problem.field} must be between ${problem.min} and ${problem.max}`
    : 'low must not be above high';
}