import { normalizeText } from './normalize.js';
import type { AllergyDetail, ReactionClass } from './options.js';
import type { Tier } from './rules.js';
/**
* CHK-6a (allergyTiers): how a recorded reaction is read. Swelling, breathing trouble and blistering skin (NICE CG183
* recommendation 1.1.1) are urgent; so is anything not readable as a milder reaction. Draft, for the clinical lead.
*/
const word = (source: string) => new RegExp(`(?<![a-z0-9])(?:${source})(?![a-z0-9])`);
/** Over-matches on purpose ("no swelling" still matches): a false urgent costs a call, a missed one can cost a life. */
const SWELLING = word('swell|swells|swelled|swelling|swollen|angioedema|angio oedema|angio edema|oedema|edema|puffy face|face swell|body swell|lips? swell|tongue swell|throat swell');
const BREATHING = word('breath|breathe|breathing|breathless|breathlessness|short of breath|shortness of breath|wheeze|wheezing|wheezy|bronchospasm|stridor|'
+ 'anaphylaxis|anaphylactic|airway|throat (?:closing|tight|tightness)|cant breathe|could not breathe|couldnt breathe|no fit breathe|collapse|collapsed');
const BLISTERING = word('blister|blisters|blistered|blistering|peel|peeling|peeled|skin (?:came off|coming off|detach[a-z]*)|stevens johnson|sjs|'
+ 'toxic epidermal necrolysis|epidermal necrolysis|mucosal|erosions?');
/** Early signs of a severe skin reaction (CG183 table 3): never read as mild. */
const NOT_MILD = word('fever|painful rash|mouth (?:sores|ulcers)|sore mouth|eye (?:sores|ulcers)|dress|organ|liver|kidney|purple');
/** Reactions that are read as not severe, when nothing above is said. */
const MILD = word('rash|rashes|hives|urticaria|itch|itchy|itching|itches|nettle rash|red skin|redness|nausea|nauseous|vomit|vomiting|vomited|'
+ 'diarrhoea|diarrhea|stomach upset|upset stomach|stomach pain|headache|thrush');
const sameEntry = (a: string, b: string) => normalizeText(a) === normalizeText(b);
/** The detail recorded for an allergy entry, if any. */
export function detailFor(allergy: string, details: readonly AllergyDetail[] | undefined): AllergyDetail | null {
return details?.find((detail) => sameEntry(detail.allergy, allergy)) ?? null;
}
/** How the engine reads a recorded reaction. With nothing recorded, unknown. */
export function classifyReaction(detail: AllergyDetail | null): ReactionClass {
if (!detail) return 'unknown';
const kind = detail.reactionKind ?? null;
if (kind === 'swelling' || kind === 'breathing' || kind === 'blistering') return kind;
if (kind === 'anaphylaxis') return 'breathing';
const text = normalizeText(detail.reaction ?? '');
if (text) {
if (SWELLING.test(text)) return 'swelling';
if (BREATHING.test(text)) return 'breathing';
if (BLISTERING.test(text)) return 'blistering';
}
if (detail.severity === 'severe') return 'severity_severe';
if (text) return !NOT_MILD.test(text) && MILD.test(text) ? 'not_severe' : 'unknown';
// A chosen mild kind, with no text to contradict it.
return kind && kind !== 'other' && kind !== 'unknown' ? 'not_severe' : 'unknown';
}
const RANK: Record<Tier, number> = { get_help_now: 4, today: 3, this_week: 2, good_to_know: 1 };
/**
* The tier of an allergy finding with allergyTiers on. A medicine with the allergy's ingredient or in its class is at
* least `today`, and `get_help_now` (Context: Urgent review) when the reaction was severe or is unknown. A possible
* cross-reaction (a cephalosporin with a penicillin allergy) keeps the rule's own tier: the PRD covers same-class matches.
*/
export function allergyTier(matchKind: 'ingredient' | 'class' | 'cross', tier: Tier, reaction: ReactionClass): Tier {
if (matchKind === 'cross') return tier;
if (reaction !== 'not_severe') return 'get_help_now';
return RANK[tier] < RANK.today ? 'today' : tier;
}
/** The recorded detail as it goes on the finding, with how it was read. */
export function reactionOutput(allergy: string, detail: AllergyDetail | null) {
return {
allergy,
reaction: detail?.reaction ?? null,
reaction_kind: detail?.reactionKind ?? null,
onset: detail?.onset ?? null,
severity: detail?.severity ?? null,
year: detail?.year ?? null,
reaction_class: classifyReaction(detail),
};
}
export type AllergyReactionOutput = ReturnType<typeof reactionOutput>;