import type { Tier } from './rules.js'; /** * Engine options for MARMAR Context (PRD v1.1, 8.5). All off unless set; with none on, runCheck gives the app exactly its * old result (tests/engine-default-equivalence.test.ts). Drafts: Context turns one on only after the clinical safety lead * signs it off (docs/engine-clinician-options.md). */ export interface CheckOptions { /** CHK-5: clinician_title and clinician_why on findings, and clinician_why on tests, from draft clinician wording. */ clinicianWording?: boolean; /** CHK-6a: a medicine in a recorded allergy's class is at least `today`, and `get_help_now` when the reaction was severe or is unknown. */ allergyTiers?: boolean; /** CHK-10: with creatinine and no eGFR, an eGFR calculated with CKD-EPI 2021 is used by the kidney rules and shown as calculated. */ egfrFromCreatinine?: boolean; /** CHK-11: metformin's dose status uses the label's kidney-adjusted limits. */ kidneyDoseLimits?: boolean; /** FND-7: each test to ask about names the medicines that need it. */ monitoringMedicines?: boolean; /** * CHK-7: findings only from rules whose current content matches a recorded sign-off. `true` accepts every * recorded sign-off; a list accepts only those sign-off ids (an empty list accepts none). */ signedOffOnly?: boolean | { acceptedSignOffs: readonly string[] }; /** CHK-3, CHK-9: what was not checked: products with no rules, facts no rule read, facts missing, rules waiting for review. */ coverage?: boolean; /** FND-2: the specific reference (label section or guideline recommendation, with its date) for each finding's rule. */ specificReferences?: boolean; } /** Moves with any change to what these options do. Separate from ENGINE_VERSION, which the app's results carry. */ export const CLINICIAN_OPTIONS_VERSION = '0.1.0-draft'; export const OPTION_NAMES = ['clinicianWording', 'allergyTiers', 'egfrFromCreatinine', 'kidneyDoseLimits', 'monitoringMedicines', 'signedOffOnly', 'coverage', 'specificReferences'] as const satisfies ReadonlyArray; /** Whether any option is on. With none, the engine takes exactly its default path. */ export function hasOptions(options: CheckOptions | undefined): options is CheckOptions { return Boolean(options) && OPTION_NAMES.some((name) => Boolean(options![name])); } /** The options that are on, for the result and its fingerprint. */ export function optionsInUse(options: CheckOptions): Record { return Object.fromEntries(OPTION_NAMES.filter((name) => Boolean(options[name])).map((name) => [name, options[name]])); } /** * What a recorded allergy was like, when known (CHK-6a). `allergy` is the entry in CheckInput.allergies it describes, * matched without case or punctuation. Every field is optional: what is missing stays unknown. */ export interface AllergyDetail { allergy: string; /** As recorded: "hives", "swelling of the lips", "could not breathe". */ reaction?: string | null; /** When the recorder chose one. */ reactionKind?: 'swelling' | 'breathing' | 'blistering' | 'anaphylaxis' | 'rash' | 'hives' | 'itching' | 'stomach' | 'other' | 'unknown' | null; onset?: 'immediate' | 'delayed' | 'unknown' | null; severity?: 'mild' | 'moderate' | 'severe' | 'unknown' | null; year?: number | null; } /** How the engine read a recorded reaction (CHK-6a): the first three, `severity_severe` and `unknown` make a class match urgent. */ export type ReactionClass = 'swelling' | 'breathing' | 'blistering' | 'severity_severe' | 'not_severe' | 'unknown'; /** Hooks the options give the engine's analysis. Absent: the default path. */ export interface EngineHooks { /** CHK-7: whether a rule may produce a finding. A rule it refuses is recorded as skipped. */ allow?: (ruleId: string) => boolean; /** CHK-6a: the tier of an allergy finding. */ allergyTier?: (context: { allergy: string; matchKind: 'ingredient' | 'class' | 'cross'; tier: Tier }) => Tier; /** CHK-11: a dose line from a kidney-adjusted limit, or null for the usual line. */ kidneyDose?: (context: { key: string; total: number | null; medicine: string }) => KidneyDoseLine | null; /** CHK-10: the eGFR the engine calculated, so findings that use it can say so. */ calculatedLab?: object | null; } export interface KidneyDoseLine { status: 'within_usual_range' | 'above_limit' | 'check_with_pharmacist'; plain: string; contraindicated: boolean; kidney_limit: KidneyLimitOutput; } /** On a dose_check line when kidneyDoseLimits set it. */ export interface KidneyLimitOutput { ingredient: string; egfr: number; egfr_comparator: '<' | '<=' | '>' | '>=' | null; egfr_observed_at: string; egfr_calculated: boolean; /** The label's band, as printed: "30-44". */ band: string; /** null when the label lists the medicine as contraindicated in the band. */ max_daily_mg: number | null; daily_mg: number | null; source_id: string; section: string; }