import type { DrugClass } from './catalog.js';
import type { CheckSourceId } from './sources.js';
/**
* Rule tables for the V2.2 check (RS1-RS10). The engine applies these; the
* writing layer only explains what they found.
*
* SIGNED OFF, WITH A TOP-UP REVIEW TO COME (signoff.ts). On 15 October 2026
* we recorded that our pharmacist, Jamila Musa, PharmD, approved the review
* pack of 13 October 2026 (rules draft-2026-10-14, engine 2.1.0) as written,
* as reported by the owner. Every rule below was in that pack and is unchanged
* since, in its clinical fields and in the wording it gives, so every rule
* carries `review: 'approved'`. Not covered, and waiting for a top-up review
* (AWAITING_TOP_UP): what changed after the pack, such as the engine 2.2.0
* dose checks and their lines, the sources for maximums, the emergency signs
* and words of intent, and too much paracetamol in symptom text. A rule that
* changes goes back to `review: 'draft'` until a new sign-off covers it
* (tests/clinical-signoff.test.ts says how).
* Tiers follow docs/app-v2/check-system-prompt.md: "today" is for pairs to act
* on the same day (label contraindications, boxed warnings, allergy matches),
* "this_week" maps to moderate interactions (Lexicomp D / BNF moderate), and
* "good_to_know" needs no change. Only about 1.6% of interactions are "avoid"
* (Nusair 2020), so "today" must stay rare.
*/
export type Tier = 'get_help_now' | 'today' | 'this_week' | 'good_to_know';
export type Evidence = 'studies' | 'few_reports' | 'theoretical';
/** An ingredient, a class, or a named product ("night nurse"); `except` leaves out ingredients that also have that class. */
export interface Matcher { ingredient?: string; class?: DrugClass; except?: DrugClass; product?: string }
export interface InteractionRule {
id: string;
a: Matcher;
b: Matcher;
tier: Tier;
evidence: Evidence;
/** Plain reason, <= 140 chars. {a} and {b} become the medicine names as the person wrote them. */
why: string;
watchFor: string[];
/**
* Which side may be stopped without waiting for a doctor, as the NHS advises.
* The engine only uses it when that medicine's origin is explicitly
* over_the_counter, and never for aspirin at an antiplatelet dose (150 mg a
* day or less) or an unknown dose. 'either' (two of the same kind): the one
* bought over the counter, only when exactly one of the two was.
*/
allowStop?: 'a' | 'b' | 'either';
/** Extra step when nothing may be stopped, e.g. "ask which one to keep". */
noStopAction?: string;
/** Say to keep taking both until a doctor or pharmacist says otherwise (a pair that is often planned). */
keepBoth?: boolean;
/**
* Side b is aspirin not recorded as prescribed, at any dose: call today
* before the next aspirin and keep only side a (NHS: no aspirin with a blood
* thinner unless a doctor says so; M-3, N5).
*/
unprescribedAspirinToday?: boolean;
/** Only when side a is more than this many mg a day, or its daily dose isn't known (the cautious side). */
aOverMgPerDay?: number;
/** What the label says to do, e.g. how far apart to take them. Shown first for good_to_know, after the tier's action otherwise. */
advice?: string;
sources: CheckSourceId[];
question: string;
clinical: string;
review: 'draft' | 'approved';
}
const approved = 'approved' as const;
export const INTERACTION_RULES: InteractionRule[] = [
// Bleeding. Aspirin of unknown dose may be a planned antiplatelet dose, so it
// gets its own rules (keep taking both, call today), never the NSAID stop wording.
{ id: 'vka-nsaid', a: { class: 'anticoagulant_vka' }, b: { class: 'nsaid', except: 'aspirin_dose_unknown' }, tier: 'today', evidence: 'studies', allowStop: 'b',
why: '{b} adds to the bleeding risk of {a} and can irritate the stomach lining.',
watchFor: ['black or bloody poo', 'vomiting blood or what looks like coffee grounds', 'unusual bruising or bleeding'],
sources: ['label-warfarin', 'nhs-warfarin', 'nhs-ibuprofen'], question: 'Which painkiller is right with {a}? Is paracetamol OK?',
clinical: 'VKA + NSAID: additive bleeding risk incl. GI bleeding; avoid, consider paracetamol.', review: approved },
{ id: 'doac-nsaid', a: { class: 'anticoagulant_doac' }, b: { class: 'nsaid', except: 'aspirin_dose_unknown' }, tier: 'today', evidence: 'studies', allowStop: 'b',
why: '{b} adds to the bleeding risk of {a}.',
watchFor: ['black or bloody poo', 'vomiting blood', 'unusual bruising or bleeding'],
sources: ['label-rivaroxaban', 'label-apixaban', 'nhs-ibuprofen'], question: 'Which painkiller is right with {a}?',
clinical: 'DOAC + NSAID: increased bleeding risk; avoid regular NSAID use.', review: approved },
{ id: 'vka-aspirin-dose-unknown', a: { class: 'anticoagulant_vka' }, b: { class: 'aspirin_dose_unknown' }, tier: 'today', evidence: 'studies', keepBoth: true,
why: '{a} and {b} both thin the blood. This may be planned, but the {b} dose isn\'t known, and painkiller doses add more to the bleeding risk.',
watchFor: ['unusual bruising or bleeding', 'black or bloody poo'],
sources: ['label-warfarin', 'nhs-warfarin', 'chest-2012'], question: 'Is {b} meant to be taken with {a}, and at what dose?',
clinical: 'VKA + aspirin of unknown dose: confirm indication and dose (antiplatelet dose may be intended; analgesic doses add NSAID GI bleeding risk).', review: approved },
{ id: 'doac-aspirin-dose-unknown', a: { class: 'anticoagulant_doac' }, b: { class: 'aspirin_dose_unknown' }, tier: 'today', evidence: 'studies', keepBoth: true,
why: '{a} and {b} both thin the blood. This may be planned, but the {b} dose isn\'t known, and painkiller doses add more to the bleeding risk.',
watchFor: ['unusual bruising or bleeding', 'black or bloody poo'],
sources: ['label-rivaroxaban', 'label-apixaban'], question: 'Is {b} meant to be taken with {a}, and at what dose?',
clinical: 'DOAC + aspirin of unknown dose: confirm indication and dose (antiplatelet dose may be intended; analgesic doses add NSAID GI bleeding risk).', review: approved },
{ id: 'anticoagulant-antiplatelet', a: { class: 'anticoagulant_vka' }, b: { class: 'antiplatelet' }, tier: 'this_week', evidence: 'studies', unprescribedAspirinToday: true,
why: '{a} and {b} both thin the blood. Sometimes this is planned, but it raises the chance of bleeding.',
watchFor: ['unusual bruising or bleeding', 'black or bloody poo'],
sources: ['label-warfarin', 'chest-2012'], question: 'Is taking {a} with {b} planned, and for how long?',
clinical: 'Anticoagulant + antiplatelet: confirm indication and duration; bleeding risk.', review: approved },
{ id: 'doac-antiplatelet', a: { class: 'anticoagulant_doac' }, b: { class: 'antiplatelet' }, tier: 'this_week', evidence: 'studies', unprescribedAspirinToday: true,
why: '{a} and {b} both thin the blood. Sometimes this is planned, but it raises the chance of bleeding.',
watchFor: ['unusual bruising or bleeding', 'black or bloody poo'],
sources: ['label-rivaroxaban', 'label-apixaban'], question: 'Is taking {a} with {b} planned, and for how long?',
clinical: 'DOAC + antiplatelet: confirm indication and duration; bleeding risk.', review: approved },
{ id: 'vka-metronidazole', a: { class: 'anticoagulant_vka' }, b: { ingredient: 'metronidazole' }, tier: 'today', evidence: 'studies',
why: '{b} can make {a} much stronger, so the blood may get too thin.',
watchFor: ['unusual bruising or bleeding', 'blood in pee or poo'],
sources: ['label-warfarin', 'nhs-warfarin'], question: 'Does my INR need checking while I take {b}?',
clinical: 'Metronidazole inhibits warfarin metabolism (CYP2C9): marked INR rise; monitor INR or choose an alternative.', review: approved },
{ id: 'vka-cotrimoxazole', a: { class: 'anticoagulant_vka' }, b: { ingredient: 'sulfamethoxazole' }, tier: 'today', evidence: 'studies',
why: '{b} can make {a} much stronger, so the blood may get too thin.',
watchFor: ['unusual bruising or bleeding', 'blood in pee or poo'],
sources: ['label-warfarin', 'nhs-warfarin'], question: 'Does my INR need checking while I take {b}?',
clinical: 'Co-trimoxazole potentiates warfarin: INR rise; monitor or avoid.', review: approved },
{ id: 'vka-azole', a: { class: 'anticoagulant_vka' }, b: { class: 'azole_antifungal' }, tier: 'today', evidence: 'studies',
why: '{b} can make {a} much stronger, so the blood may get too thin.',
watchFor: ['unusual bruising or bleeding', 'blood in pee or poo'],
sources: ['label-warfarin'], question: 'Does my INR need checking while I take {b}?',
clinical: 'Azole antifungals inhibit warfarin metabolism: INR rise; monitor INR.', review: approved },
{ id: 'vka-fluoroquinolone', a: { class: 'anticoagulant_vka' }, b: { class: 'fluoroquinolone' }, tier: 'this_week', evidence: 'few_reports',
why: '{b} can make {a} stronger in some people.',
watchFor: ['unusual bruising or bleeding'],
sources: ['label-warfarin', 'label-fluoroquinolone'], question: 'Should my INR be checked during the {b} course?',
clinical: 'Fluoroquinolones may raise INR with warfarin; consider INR check.', review: approved },
{ id: 'vka-macrolide', a: { class: 'anticoagulant_vka' }, b: { class: 'strong_cyp3a4_inhibitor' }, tier: 'this_week', evidence: 'few_reports',
why: '{b} can make {a} stronger.',
watchFor: ['unusual bruising or bleeding'],
sources: ['label-warfarin'], question: 'Should my INR be checked during the {b} course?',
clinical: 'Macrolides/strong CYP3A4 inhibitors may raise INR with warfarin; monitor.', review: approved },
{ id: 'vka-inducer', a: { class: 'anticoagulant_vka' }, b: { class: 'strong_enzyme_inducer' }, tier: 'this_week', evidence: 'studies',
why: '{b} can make {a} weaker, so it may not protect as well.',
watchFor: ['signs of a clot: a swollen, painful leg, or sudden breathlessness'],
sources: ['label-warfarin'], question: 'Does my {a} dose or INR need checking because of {b}?',
clinical: 'Enzyme inducer reduces warfarin effect; INR monitoring and dose adjustment.', review: approved },
{ id: 'vka-amiodarone', a: { class: 'anticoagulant_vka' }, b: { ingredient: 'amiodarone' }, tier: 'this_week', evidence: 'studies',
why: '{b} makes {a} stronger, often over several weeks.',
watchFor: ['unusual bruising or bleeding'],
sources: ['label-warfarin'], question: 'Is my {a} dose right with {b}?',
clinical: 'Amiodarone potentiates warfarin; typically requires dose reduction and INR monitoring.', review: approved },
{ id: 'nsaid-nsaid', a: { class: 'nsaid' }, b: { class: 'nsaid' }, tier: 'today', evidence: 'studies', allowStop: 'either',
noStopAction: 'Ask which one painkiller to keep taking.',
why: '{a} and {b} are the same kind of painkiller. Taking both raises the risk of stomach bleeding and kidney problems.',
watchFor: ['black or bloody poo', 'stomach pain', 'peeing much less than usual'],
sources: ['nhs-ibuprofen', 'label-nsaid'], question: 'Which one painkiller should I take?',
clinical: 'Two NSAIDs: duplicate class; GI and renal toxicity; use one.', review: approved },
{ id: 'ssri-nsaid', a: { class: 'ssri' }, b: { class: 'nsaid' }, tier: 'this_week', evidence: 'studies',
why: '{a} with {b} raises the chance of stomach bleeding.',
watchFor: ['black or bloody poo', 'stomach pain'],
sources: ['label-nsaid'], question: 'Do I need stomach protection, or a different painkiller?',
clinical: 'SSRI + NSAID: increased upper GI bleeding risk; consider PPI or alternative analgesic.', review: approved },
{ id: 'ssri-anticoagulant', a: { class: 'ssri' }, b: { class: 'anticoagulant_vka' }, tier: 'this_week', evidence: 'studies',
why: '{a} with {b} raises the chance of bleeding.',
watchFor: ['unusual bruising or bleeding'],
sources: ['label-warfarin'], question: 'Should my INR be checked more often?',
clinical: 'SSRI + warfarin: increased bleeding risk; monitor.', review: approved },
{ id: 'herbal-bleeding-anticoagulant', a: { class: 'herbal_bleeding_risk' }, b: { class: 'anticoagulant_vka' }, tier: 'good_to_know', evidence: 'few_reports',
why: 'In larger amounts, {a} may add a little to the blood-thinning effect of {b}.',
watchFor: ['unusual bruising or bleeding'],
sources: ['nhs-warfarin'], question: 'Is the amount of {a} I take OK with {b}?',
clinical: 'Case reports of potentiated anticoagulation with garlic/ginger/turmeric/ginkgo at supplement doses.', review: approved },
{ id: 'herbal-bleeding-doac', a: { class: 'herbal_bleeding_risk' }, b: { class: 'anticoagulant_doac' }, tier: 'good_to_know', evidence: 'theoretical',
why: 'In larger amounts, {a} may add a little to the blood-thinning effect of {b}.',
watchFor: ['unusual bruising or bleeding'],
sources: ['marmar-herbal-doac'], question: 'Is the amount of {a} I take OK with {b}?',
clinical: 'Theoretical additive antiplatelet effect of herbal supplements with DOACs.', review: approved },
{ id: 'st-johns-wort-anticoagulant', a: { class: 'st_johns_wort' }, b: { class: 'anticoagulant_vka' }, tier: 'this_week', evidence: 'studies',
why: '{a} can make {b} weaker.',
watchFor: ['signs of a clot: a swollen, painful leg, or sudden breathlessness'],
sources: ['nhs-st-johns-wort', 'label-warfarin'], question: 'Should I keep taking {a} with {b}?',
clinical: "St John's wort induces CYP enzymes, reducing warfarin effect.", review: approved },
{ id: 'st-johns-wort-ssri', a: { class: 'st_johns_wort' }, b: { class: 'ssri' }, tier: 'this_week', evidence: 'studies',
why: '{a} with {b} can cause too much serotonin.',
watchFor: ['shaking', 'sweating', 'a fast heartbeat', 'confusion'],
sources: ['nhs-st-johns-wort'], question: 'Should I keep taking {a} with {b}?',
clinical: "St John's wort + SSRI: serotonin toxicity risk.", review: approved },
// Kidneys and potassium
{ id: 'acei-arb', a: { class: 'acei' }, b: { class: 'arb' }, tier: 'this_week', evidence: 'studies',
why: '{a} and {b} work in a similar way. Together they can affect the kidneys and potassium.',
watchFor: ['feeling dizzy or faint', 'peeing much less than usual'],
sources: ['nice-cks-ace'], question: 'Do I need both {a} and {b}?',
clinical: 'ACEi + ARB dual RAAS blockade: not recommended; hyperkalaemia, AKI.', review: approved },
{ id: 'raas-k-sparing', a: { class: 'acei' }, b: { class: 'k_sparing' }, tier: 'this_week', evidence: 'studies',
why: '{a} with {b} can raise potassium in the blood.',
watchFor: ['muscle weakness', 'an irregular heartbeat'],
sources: ['nice-cks-ace'], question: 'When should my potassium be checked?',
clinical: 'ACEi + K-sparing diuretic: hyperkalaemia risk; monitor U&E.', review: approved },
{ id: 'arb-k-sparing', a: { class: 'arb' }, b: { class: 'k_sparing' }, tier: 'this_week', evidence: 'studies',
why: '{a} with {b} can raise potassium in the blood.',
watchFor: ['muscle weakness', 'an irregular heartbeat'],
sources: ['nice-cks-ace'], question: 'When should my potassium be checked?',
clinical: 'ARB + K-sparing diuretic: hyperkalaemia risk; monitor U&E.', review: approved },
{ id: 'raas-potassium', a: { class: 'acei' }, b: { class: 'potassium_supplement' }, tier: 'this_week', evidence: 'studies',
why: '{a} with {b} can raise potassium in the blood.',
watchFor: ['muscle weakness', 'an irregular heartbeat'],
sources: ['nice-cks-ace'], question: 'Do I still need {b} with {a}?',
clinical: 'ACEi + potassium supplement: hyperkalaemia risk.', review: approved },
{ id: 'arb-potassium', a: { class: 'arb' }, b: { class: 'potassium_supplement' }, tier: 'this_week', evidence: 'studies',
why: '{a} with {b} can raise potassium in the blood.',
watchFor: ['muscle weakness', 'an irregular heartbeat'],
sources: ['nice-cks-ace'], question: 'Do I still need {b} with {a}?',
clinical: 'ARB + potassium supplement: hyperkalaemia risk.', review: approved },
{ id: 'nsaid-acei', a: { class: 'nsaid' }, b: { class: 'acei' }, tier: 'this_week', evidence: 'studies',
why: '{a} can weaken {b} and, with it, put strain on the kidneys.',
watchFor: ['peeing much less than usual', 'swollen ankles'],
sources: ['label-nsaid', 'nice-cks-ace'], question: 'Is there a painkiller that is easier on the kidneys?',
clinical: 'NSAID + ACEi: reduced antihypertensive effect, AKI risk (worse with diuretic).', review: approved },
{ id: 'nsaid-arb', a: { class: 'nsaid' }, b: { class: 'arb' }, tier: 'this_week', evidence: 'studies',
why: '{a} can weaken {b} and, with it, put strain on the kidneys.',
watchFor: ['peeing much less than usual', 'swollen ankles'],
sources: ['label-nsaid', 'nice-cks-ace'], question: 'Is there a painkiller that is easier on the kidneys?',
clinical: 'NSAID + ARB: reduced antihypertensive effect, AKI risk (worse with diuretic).', review: approved },
// Muscles, heart rhythm and blood pressure
{ id: 'simvastatin-strong-cyp3a4', a: { ingredient: 'simvastatin' }, b: { class: 'strong_cyp3a4_inhibitor' }, tier: 'today', evidence: 'studies',
why: '{b} raises the level of {a} in the body, which can damage muscles.',
watchFor: ['unexplained muscle pain or weakness', 'dark, tea-coloured pee'],
sources: ['label-simvastatin'], question: 'Is {a} OK while I take {b}?',
clinical: 'Simvastatin + strong CYP3A4 inhibitor: contraindicated (label); rhabdomyolysis risk.', review: approved },
{ id: 'atorvastatin-strong-cyp3a4', a: { ingredient: 'atorvastatin' }, b: { class: 'strong_cyp3a4_inhibitor' }, tier: 'this_week', evidence: 'studies',
why: '{b} raises the level of {a}, which can affect muscles.',
watchFor: ['unexplained muscle pain or weakness'],
sources: ['label-atorvastatin'], question: 'Is my {a} dose right while I take {b}?',
clinical: 'Atorvastatin + strong CYP3A4 inhibitor: limit dose; myopathy risk.', review: approved },
{ id: 'simvastatin-amiodarone', a: { ingredient: 'simvastatin' }, b: { ingredient: 'amiodarone' }, tier: 'this_week', evidence: 'studies',
why: '{b} raises the level of {a}, which can affect muscles.',
watchFor: ['unexplained muscle pain or weakness'],
sources: ['label-simvastatin'], question: 'Is my {a} dose right with {b}?',
clinical: 'Simvastatin with amiodarone: label limits simvastatin to 20 mg/day.', review: approved },
{ id: 'digoxin-macrolide', a: { ingredient: 'digoxin' }, b: { class: 'strong_cyp3a4_inhibitor' }, tier: 'this_week', evidence: 'studies',
why: '{b} can raise the level of {a} too high.',
watchFor: ['feeling sick', 'blurred or yellow vision', 'a slow or irregular heartbeat'],
sources: ['label-digoxin'], question: 'Should my {a} level be checked?',
clinical: 'Clarithromycin/erythromycin increase digoxin levels (P-gp); toxicity risk.', review: approved },
{ id: 'digoxin-amiodarone', a: { ingredient: 'digoxin' }, b: { ingredient: 'amiodarone' }, tier: 'this_week', evidence: 'studies',
why: '{b} raises the level of {a}.',
watchFor: ['feeling sick', 'blurred or yellow vision', 'a slow or irregular heartbeat'],
sources: ['label-digoxin'], question: 'Is my {a} dose right with {b}?',
clinical: 'Amiodarone increases digoxin levels; usually halve digoxin dose and monitor.', review: approved },
{ id: 'pde5-nitrate', a: { class: 'pde5_inhibitor' }, b: { class: 'nitrate' }, tier: 'today', evidence: 'studies',
why: '{a} with {b} can make blood pressure drop dangerously.',
watchFor: ['fainting', 'feeling very dizzy', 'chest pain'],
sources: ['label-sildenafil'], question: 'Should I take {a} at all with {b}?',
clinical: 'PDE5 inhibitor + nitrate: contraindicated; severe hypotension.', review: approved },
// Sedation, breathing and serotonin
{ id: 'opioid-benzodiazepine', a: { class: 'opioid' }, b: { class: 'benzodiazepine' }, tier: 'today', evidence: 'studies',
why: '{a} with {b} can cause deep sleepiness and slow breathing.',
watchFor: ['very sleepy or hard to wake', 'slow or shallow breathing'],
sources: ['fda-opioid-benzodiazepine-2016'], question: 'Do I need both {a} and {b}?',
clinical: 'Opioid + benzodiazepine: FDA boxed warning; respiratory depression.', review: approved },
{ id: 'tramadol-ssri', a: { class: 'serotonergic_opioid' }, b: { class: 'ssri' }, tier: 'this_week', evidence: 'studies',
why: '{a} with {b} can cause too much serotonin and, in some people, fits.',
watchFor: ['shaking', 'sweating', 'a fast heartbeat', 'confusion'],
sources: ['label-tramadol'], question: 'Is there another painkiller that suits me with {b}?',
clinical: 'Tramadol + SSRI: serotonin syndrome and seizure threshold.', review: approved },
{ id: 'tramadol-tca', a: { class: 'serotonergic_opioid' }, b: { class: 'tca' }, tier: 'this_week', evidence: 'studies',
why: '{a} with {b} can cause too much serotonin and, in some people, fits.',
watchFor: ['shaking', 'sweating', 'confusion'],
sources: ['label-tramadol'], question: 'Is there another painkiller that suits me with {b}?',
clinical: 'Tramadol + TCA: serotonergic and seizure risk.', review: approved },
// Blood counts, kidneys and levels
{ id: 'methotrexate-trimethoprim', a: { ingredient: 'methotrexate' }, b: { class: 'trimethoprim' }, tier: 'today', evidence: 'studies',
why: '{b} with {a} can harm the blood and bone marrow.',
watchFor: ['sore throat or fever', 'mouth ulcers', 'unusual bruising'],
sources: ['label-methotrexate'], question: 'Should a different antibiotic be used?',
clinical: 'Methotrexate + trimethoprim/co-trimoxazole: myelosuppression; avoid.', review: approved },
{ id: 'methotrexate-nsaid', a: { ingredient: 'methotrexate' }, b: { class: 'nsaid' }, tier: 'this_week', evidence: 'studies',
why: '{b} can raise the level of {a}.',
watchFor: ['mouth ulcers', 'sore throat or fever'],
sources: ['label-methotrexate'], question: 'Is {b} OK with my {a} dose?',
clinical: 'NSAIDs reduce methotrexate clearance; risk higher with higher doses.', review: approved },
{ id: 'allopurinol-azathioprine', a: { ingredient: 'allopurinol' }, b: { ingredient: 'azathioprine' }, tier: 'today', evidence: 'studies',
why: '{a} makes {b} much stronger, which can harm the blood.',
watchFor: ['sore throat or fever', 'unusual bruising'],
sources: ['label-azathioprine'], question: 'Is my {b} dose right with {a}?',
clinical: 'Allopurinol inhibits xanthine oxidase: azathioprine dose must be reduced to about a quarter.', review: approved },
{ id: 'lithium-nsaid', a: { ingredient: 'lithium' }, b: { class: 'nsaid' }, tier: 'this_week', evidence: 'studies',
why: '{b} can raise the level of {a} too high.',
watchFor: ['shaking hands', 'feeling sick or having diarrhoea', 'confusion'],
sources: ['label-lithium'], question: 'Should my {a} level be checked?',
clinical: 'NSAIDs reduce lithium clearance; monitor levels.', review: approved },
{ id: 'lithium-acei', a: { ingredient: 'lithium' }, b: { class: 'acei' }, tier: 'this_week', evidence: 'studies',
why: '{b} can raise the level of {a}.',
watchFor: ['shaking hands', 'confusion'],
sources: ['label-lithium'], question: 'Should my {a} level be checked?',
clinical: 'ACEi raise lithium levels; monitor.', review: approved },
{ id: 'lithium-arb', a: { ingredient: 'lithium' }, b: { class: 'arb' }, tier: 'this_week', evidence: 'studies',
why: '{b} can raise the level of {a}.',
watchFor: ['shaking hands', 'confusion'],
sources: ['label-lithium'], question: 'Should my {a} level be checked?',
clinical: 'ARBs raise lithium levels; monitor.', review: approved },
{ id: 'lithium-thiazide', a: { ingredient: 'lithium' }, b: { class: 'thiazide' }, tier: 'this_week', evidence: 'studies',
why: '{b} can raise the level of {a}.',
watchFor: ['shaking hands', 'confusion'],
sources: ['label-lithium'], question: 'Should my {a} level be checked?',
clinical: 'Thiazides reduce lithium clearance; monitor levels.', review: approved },
// Sugar
{ id: 'sulfonylurea-cotrimoxazole', a: { class: 'sulfonylurea' }, b: { ingredient: 'sulfamethoxazole' }, tier: 'this_week', evidence: 'few_reports',
why: '{b} can make {a} lower blood sugar too much.',
watchFor: ['shaking', 'sweating', 'feeling confused or very hungry'],
sources: ['label-sulfonylurea'], question: 'Should I check my sugar more often during the {b} course?',
clinical: 'Co-trimoxazole may potentiate sulfonylureas: hypoglycaemia.', review: approved },
{ id: 'sulfonylurea-fluconazole', a: { class: 'sulfonylurea' }, b: { ingredient: 'fluconazole' }, tier: 'this_week', evidence: 'few_reports',
why: '{b} can make {a} lower blood sugar too much.',
watchFor: ['shaking', 'sweating', 'feeling confused or very hungry'],
sources: ['label-sulfonylurea'], question: 'Should I check my sugar more often during the {b} course?',
clinical: 'Fluconazole inhibits sulfonylurea metabolism: hypoglycaemia.', review: approved },
// Other common pairs
{ id: 'clopidogrel-omeprazole', a: { ingredient: 'clopidogrel' }, b: { ingredient: 'omeprazole' }, tier: 'this_week', evidence: 'studies',
why: '{b} can make {a} work less well.',
watchFor: [],
sources: ['label-clopidogrel'], question: 'Is there a stomach medicine that works better with {a}?',
clinical: 'Omeprazole/esomeprazole inhibit CYP2C19 activation of clopidogrel; label advises avoiding.', review: approved },
{ id: 'clopidogrel-esomeprazole', a: { ingredient: 'clopidogrel' }, b: { ingredient: 'esomeprazole' }, tier: 'this_week', evidence: 'studies',
why: '{b} can make {a} work less well.',
watchFor: [],
sources: ['label-clopidogrel'], question: 'Is there a stomach medicine that works better with {a}?',
clinical: 'Esomeprazole inhibits CYP2C19 activation of clopidogrel; label advises avoiding.', review: approved },
{ id: 'dolutegravir-rifampicin', a: { ingredient: 'dolutegravir' }, b: { ingredient: 'rifampicin' }, tier: 'this_week', evidence: 'studies',
why: '{b} lowers the level of {a}, so it may not work as well.',
watchFor: [],
sources: ['label-dolutegravir'], question: 'Is my {a} dose right while I take {b}?',
clinical: 'Rifampicin induces dolutegravir metabolism: dolutegravir 50 mg twice daily per label.', review: approved },
{ id: 'dolutegravir-cations', a: { ingredient: 'dolutegravir' }, b: { class: 'divalent_cation' }, tier: 'this_week', evidence: 'studies',
advice: 'Take {a} 2 hours before, or 6 hours after, {b}.',
why: '{b} can keep {a} from being absorbed if they are taken at the same time, so {a} may not work as well.',
watchFor: [],
sources: ['label-dolutegravir'], question: 'Should I take {a} 2 hours before or 6 hours after {b}?',
clinical: 'Polyvalent cations (iron, calcium, zinc, magnesium, aluminium antacids) chelate dolutegravir: take dolutegravir 2 h before or 6 h after (label; iron or calcium may be taken together with food).', review: approved },
{ id: 'lumefantrine-efavirenz', a: { ingredient: 'lumefantrine' }, b: { ingredient: 'efavirenz' }, tier: 'this_week', evidence: 'studies',
why: '{b} can lower the levels of {a} in the blood, so the malaria treatment may not work as well.',
watchFor: ['a fever that comes back after the course'],
sources: ['label-artemether-lumefantrine', 'liverpool-efavirenz-artemether-lumefantrine'], question: 'Will {a} still work well while I take {b}?',
clinical: 'Efavirenz (CYP3A4 inducer) lowers artemether, DHA and lumefantrine exposure: risk of treatment failure; use with caution, check response.', review: approved },
// Strong inducers (rifampicin, carbamazepine, phenytoin) and St John's wort: the apixaban and rivaroxaban labels say avoid.
{ id: 'apixaban-strong-inducer', a: { ingredient: 'apixaban' }, b: { class: 'strong_enzyme_inducer' }, tier: 'today', evidence: 'studies',
why: '{b} makes {a} much weaker, so it may not protect against clots.',
watchFor: ['signs of a clot: a swollen, painful leg, or sudden breathlessness'],
sources: ['label-apixaban'], question: 'Is {a} the right blood thinner while I take {b}?',
clinical: 'Combined P-gp and strong CYP3A4 inducer (rifampicin, carbamazepine, phenytoin) markedly lowers apixaban exposure: avoid (label).', review: approved },
{ id: 'apixaban-st-johns-wort', a: { ingredient: 'apixaban' }, b: { class: 'st_johns_wort' }, tier: 'today', evidence: 'studies',
why: '{b} makes {a} much weaker, so it may not protect against clots.',
watchFor: ['signs of a clot: a swollen, painful leg, or sudden breathlessness'],
sources: ['label-apixaban'], question: 'Is {a} the right blood thinner while I take {b}?',
clinical: "St John's wort (combined P-gp and strong CYP3A4 inducer) lowers apixaban exposure: avoid (label).", review: approved },
{ id: 'rivaroxaban-strong-inducer', a: { ingredient: 'rivaroxaban' }, b: { class: 'strong_enzyme_inducer' }, tier: 'today', evidence: 'studies',
why: '{b} makes {a} much weaker, so it may not protect against clots.',
watchFor: ['signs of a clot: a swollen, painful leg, or sudden breathlessness'],
sources: ['label-rivaroxaban'], question: 'Is {a} the right blood thinner while I take {b}?',
clinical: 'Combined P-gp and strong CYP3A4 inducer (rifampicin, carbamazepine, phenytoin) markedly lowers rivaroxaban exposure: avoid (label).', review: approved },
{ id: 'rivaroxaban-st-johns-wort', a: { ingredient: 'rivaroxaban' }, b: { class: 'st_johns_wort' }, tier: 'today', evidence: 'studies',
why: '{b} makes {a} much weaker, so it may not protect against clots.',
watchFor: ['signs of a clot: a swollen, painful leg, or sudden breathlessness'],
sources: ['label-rivaroxaban'], question: 'Is {a} the right blood thinner while I take {b}?',
clinical: "St John's wort (combined P-gp and strong CYP3A4 inducer) lowers rivaroxaban exposure: avoid (label).", review: approved },
// Malaria treatment (Coartem label: strong CYP3A4 inducers contraindicated; WHO: avoid artesunate-amodiaquine with efavirenz).
{ id: 'lumefantrine-strong-inducer', a: { ingredient: 'lumefantrine' }, b: { class: 'strong_enzyme_inducer' }, tier: 'today', evidence: 'studies',
why: '{b} makes {a} much weaker, so the malaria may not be cured.',
watchFor: ['a fever that comes back after the course'],
sources: ['label-artemether-lumefantrine'], question: 'Is {a} the right malaria treatment while I take {b}?',
clinical: 'Artemether-lumefantrine with a strong CYP3A4 inducer (rifampicin, carbamazepine, phenytoin): contraindicated (label); loss of efficacy.', review: approved },
{ id: 'lumefantrine-st-johns-wort', a: { ingredient: 'lumefantrine' }, b: { class: 'st_johns_wort' }, tier: 'today', evidence: 'studies',
why: '{b} makes {a} much weaker, so the malaria may not be cured.',
watchFor: ['a fever that comes back after the course'],
sources: ['label-artemether-lumefantrine'], question: 'Is {a} the right malaria treatment while I take {b}?',
clinical: "Artemether-lumefantrine with St John's wort (CYP3A4 inducer): contraindicated (label); loss of efficacy.", review: approved },
{ id: 'amodiaquine-efavirenz', a: { ingredient: 'amodiaquine' }, b: { ingredient: 'efavirenz' }, tier: 'today', evidence: 'few_reports',
why: '{a} with {b} can harm the liver.',
watchFor: ['yellow skin or eyes', 'dark pee', 'stomach pain'],
sources: ['who-malaria-guidelines'], question: 'Is there a malaria treatment that suits me better with {b}?',
clinical: 'Artesunate-amodiaquine with efavirenz: marked hepatotoxicity reported; WHO advises avoiding ASAQ in people on efavirenz.', review: approved },
// HIV treatment.
{ id: 'dolutegravir-phenytoin', a: { ingredient: 'dolutegravir' }, b: { ingredient: 'phenytoin' }, tier: 'today', evidence: 'studies',
why: '{b} makes {a} much weaker, so the HIV treatment may not work as well.',
watchFor: [],
sources: ['label-dolutegravir'], question: 'Is {a} right for me while I take {b}?',
clinical: 'Phenytoin (UGT1A1/CYP3A inducer) lowers dolutegravir exposure: avoid coadministration (label).', review: approved },
{ id: 'dolutegravir-st-johns-wort', a: { ingredient: 'dolutegravir' }, b: { class: 'st_johns_wort' }, tier: 'today', evidence: 'studies',
why: '{b} makes {a} much weaker, so the HIV treatment may not work as well.',
watchFor: [],
sources: ['label-dolutegravir'], question: 'Is {a} right for me while I take {b}?',
clinical: "St John's wort lowers dolutegravir exposure: avoid coadministration (label).", review: approved },
{ id: 'dolutegravir-carbamazepine', a: { ingredient: 'dolutegravir' }, b: { ingredient: 'carbamazepine' }, tier: 'this_week', evidence: 'studies',
why: '{b} lowers the level of {a}, so it may not work as well.',
watchFor: [],
sources: ['label-dolutegravir'], question: 'Is my {a} dose right while I take {b}?',
clinical: 'Carbamazepine lowers dolutegravir exposure: dolutegravir 50 mg twice daily per label.', review: approved },
{ id: 'nevirapine-rifampicin', a: { ingredient: 'nevirapine' }, b: { ingredient: 'rifampicin' }, tier: 'today', evidence: 'studies',
why: '{b} makes {a} much weaker, so the HIV treatment may not work as well.',
watchFor: [],
sources: ['label-nevirapine'], question: 'Is {a} right for me while I take {b}?',
clinical: 'Rifampicin lowers nevirapine exposure (AUC about 58% lower): coadministration not recommended (label).', review: approved },
// Heart, muscles and stomach.
{ id: 'simvastatin-amlodipine', a: { ingredient: 'simvastatin' }, b: { ingredient: 'amlodipine' }, tier: 'this_week', evidence: 'studies', aOverMgPerDay: 20,
why: 'With {b}, the {a} label limit is 20 mg a day, because more can damage muscles.',
watchFor: ['unexplained muscle pain or weakness', 'dark, tea-coloured pee'],
sources: ['label-simvastatin'], question: 'Is my {a} dose right with {b}?',
clinical: 'Simvastatin with amlodipine: do not exceed simvastatin 20 mg/day (label); myopathy risk. Shown when the dose is above 20 mg or unknown.', review: approved },
{ id: 'digoxin-loop-diuretic', a: { ingredient: 'digoxin' }, b: { class: 'loop_diuretic' }, tier: 'this_week', evidence: 'studies',
why: '{b} can lower potassium in the blood, and low potassium makes side effects of {a} more likely.',
watchFor: ['feeling sick', 'blurred or yellow vision', 'a slow or irregular heartbeat'],
sources: ['label-digoxin'], question: 'Should my potassium and {a} level be checked?',
clinical: 'Loop diuretic with digoxin: hypokalaemia and hypomagnesaemia increase digoxin toxicity; monitor potassium and digoxin level.', review: approved },
{ id: 'digoxin-thiazide', a: { ingredient: 'digoxin' }, b: { class: 'thiazide' }, tier: 'this_week', evidence: 'studies',
why: '{b} can lower potassium in the blood, and low potassium makes side effects of {a} more likely.',
watchFor: ['feeling sick', 'blurred or yellow vision', 'a slow or irregular heartbeat'],
sources: ['label-digoxin'], question: 'Should my potassium and {a} level be checked?',
clinical: 'Thiazide with digoxin: hypokalaemia increases digoxin toxicity; monitor potassium and digoxin level.', review: approved },
{ id: 'corticosteroid-nsaid', a: { class: 'corticosteroid' }, b: { class: 'nsaid' }, tier: 'this_week', evidence: 'studies',
why: '{a} with {b} raises the chance of stomach ulcers and bleeding.',
watchFor: ['black or bloody poo', 'stomach pain'],
sources: ['label-nsaid'], question: 'Do I need stomach protection while I take {a} and {b}?',
clinical: 'Corticosteroid + NSAID: increased risk of GI ulceration and bleeding (NSAID labels); consider gastroprotection.', review: approved },
{ id: 'fluoroquinolone-cations', a: { class: 'fluoroquinolone' }, b: { class: 'divalent_cation' }, tier: 'good_to_know', evidence: 'studies',
advice: 'Take {a} at least 2 hours before, or 6 hours after, {b}.',
why: '{b} keeps {a} from being absorbed if they are taken together.',
watchFor: [],
sources: ['label-fluoroquinolone'], question: 'How far apart should I take {a} and {b}?',
clinical: 'Chelation: separate fluoroquinolone from polyvalent cations (2 h before or 6 h after).', review: approved },
{ id: 'levothyroxine-cations', a: { class: 'thyroid_hormone' }, b: { class: 'divalent_cation' }, tier: 'good_to_know', evidence: 'studies',
advice: 'Take {a} at least 4 hours apart from {b}.',
why: '{b} can keep {a} from being absorbed if they are taken together.',
watchFor: [],
sources: ['label-levothyroxine'], question: 'How far apart should I take {a} and {b}?',
clinical: 'Separate levothyroxine from iron/calcium/antacids by about 4 hours.', review: approved },
];
export interface AllergyClassRule {
id: string;
/** Words a person uses for the allergy, matched whole-word. */
allergyTerms: string[];
/** Medicines that match the allergy directly. */
matches: Matcher[];
/** Medicines with a small chance of cross-reaction. */
crossReactive?: Matcher[];
/** How soon to act on a cross-reaction (default this_week). */
crossReactiveTier?: Tier;
/** For the pharmacist's review. */
notes?: string;
sources: CheckSourceId[];
review: 'draft' | 'approved';
}
export const ALLERGY_RULES: AllergyClassRule[] = [
{ id: 'allergy-penicillin', allergyTerms: ['penicillin', 'penicillins', 'amoxicillin', 'amoxil', 'amoxyl', 'augmentin', 'ampicillin', 'ampiclox', 'flucloxacillin', 'cloxacillin'],
matches: [{ class: 'penicillin' }], crossReactive: [{ class: 'cephalosporin' }], crossReactiveTier: 'today',
notes: 'Pharmacist to review: a cephalosporin with a recorded penicillin allergy is call-first (today), because how severe the allergy was is not recorded.',
sources: ['nhs-anaphylaxis'], review: approved },
// "Sulphur drugs" is how many people say sulfa; there is no allergy to sulphur itself, so it is read as sulfa.
{ id: 'allergy-sulfa', allergyTerms: ['sulfa', 'sulpha', 'sulfa drugs', 'sulpha drugs', 'sulphur', 'sulfur', 'sulphur drugs', 'sulfur drugs', 'sulfonamide', 'sulphonamide',
'septrin', 'septrine', 'bactrim', 'co-trimoxazole', 'cotrimoxazole', 'fansidar'],
matches: [{ class: 'sulfonamide_antibiotic' }], sources: ['nhs-anaphylaxis'], review: approved },
{ id: 'allergy-nsaid', allergyTerms: ['nsaid', 'nsaids', 'aspirin', 'ibuprofen', 'brufen', 'diclofenac', 'naproxen', 'piroxicam', 'meloxicam', 'mefenamic acid', 'ponstan',
'indometacin', 'indomethacin', 'ketoprofen', 'anti-inflammatory', 'anti-inflammatories'],
matches: [{ class: 'nsaid' }, { class: 'topical_nsaid' }, { ingredient: 'aspirin' }], sources: ['nhs-anaphylaxis'], review: approved },
{ id: 'allergy-cephalosporin', allergyTerms: ['cephalosporin', 'cephalosporins', 'ceftriaxone', 'cefuroxime', 'cefalexin', 'cephalexin'],
matches: [{ class: 'cephalosporin' }], sources: ['nhs-anaphylaxis'], review: approved },
{ id: 'allergy-opioid', allergyTerms: ['codeine', 'morphine', 'tramadol', 'opioid', 'opioids'],
matches: [{ class: 'opioid' }], sources: ['nhs-anaphylaxis'], review: approved },
];
export interface OlderAdultRule {
id: string;
match: Matcher;
tier: Tier;
why: string;
watchFor: string[];
sources: CheckSourceId[];
question: string;
clinical: string;
review: 'draft' | 'approved';
}
/** RS8: for people 65 and over. Always "worth reviewing", never "stop". */
export const OLDER_ADULT_RULES: OlderAdultRule[] = [
{ id: 'older-long-acting-sulfonylurea', match: { class: 'long_acting_sulfonylurea' }, tier: 'this_week',
why: 'After 65, {a} can cause long-lasting low blood sugar.', watchFor: ['shaking', 'sweating', 'confusion, especially in the morning'],
sources: ['beers-2023', 'stopp-start-v3'], question: 'Is {a} still the best choice for my sugar?',
clinical: 'Beers 2023 / STOPP: avoid long-acting sulfonylureas (glyburide, glimepiride) in older adults: prolonged hypoglycaemia.', review: approved },
{ id: 'older-first-gen-antihistamine', match: { class: 'first_gen_antihistamine' }, tier: 'this_week',
why: 'After 65, {a} can cause confusion, dry mouth and falls.', watchFor: ['confusion', 'falls', 'trouble peeing'],
sources: ['beers-2023'], question: 'Is there an allergy or sleep medicine that suits me better?',
clinical: 'Beers 2023: first-generation antihistamines are strongly anticholinergic; avoid.', review: approved },
{ id: 'older-benzodiazepine', match: { class: 'benzodiazepine' }, tier: 'this_week',
why: 'After 65, {a} raises the chance of falls and memory problems.', watchFor: ['falls', 'confusion', 'daytime sleepiness'],
sources: ['beers-2023', 'stopp-start-v3'], question: 'Can we talk about slowly reducing {a}?',
clinical: 'Beers 2023: avoid benzodiazepines (cognitive impairment, falls, fractures). Taper, do not stop abruptly.', review: approved },
{ id: 'older-tca', match: { class: 'tca' }, tier: 'this_week',
why: 'After 65, {a} can cause confusion, dizziness and falls.', watchFor: ['confusion', 'dizziness on standing', 'falls'],
sources: ['beers-2023'], question: 'Is {a} still the right medicine for me?',
clinical: 'Beers 2023: amitriptyline is highly anticholinergic and sedating; avoid.', review: approved },
{ id: 'older-regular-nsaid', match: { class: 'nsaid' }, tier: 'this_week',
why: 'After 65, regular {a} raises the chance of stomach bleeding and kidney problems.', watchFor: ['black or bloody poo', 'stomach pain'],
sources: ['beers-2023', 'stopp-start-v3'], question: 'Do I need stomach protection, or a different painkiller?',
clinical: 'Beers 2023: avoid chronic NSAID use unless alternatives fail and a gastroprotective agent is used.', review: approved },
];
export interface ChildRule {
id: string;
match: Matcher;
/** Applies when the person could be younger than this (the youngest they could be, when only the year is known). */
under: number;
/** Only when the medicine's name has one of these words (cough and cold products). */
nameWords?: string[];
tier: Tier;
why: string;
watchFor: string[];
sources: CheckSourceId[];
question: string;
clinical: string;
review: 'draft' | 'approved';
}
/** H5: medicines that aren't for children. Never "stop": call before the next dose. */
export const CHILD_RULES: ChildRule[] = [
{ id: 'child-aspirin-under-16', match: { ingredient: 'aspirin' }, under: 16, tier: 'today',
why: '{a} isn\'t for children under 16 unless a doctor prescribes it. Aspirin is linked to Reye\'s syndrome, a serious illness.',
watchFor: ['being sick over and over', 'confusion', 'unusual sleepiness'],
sources: ['nhs-aspirin-children', 'mhra-aspirin-children'], question: 'Is {a} right for a child of this age?',
clinical: 'Aspirin under 16 years: avoid unless specifically indicated (e.g. Kawasaki disease, after cardiac surgery) because of Reye\'s syndrome (MHRA, NHS).', review: approved },
{ id: 'child-codeine-under-12', match: { ingredient: 'codeine' }, under: 12, tier: 'today',
why: '{a} isn\'t for children under 12: in some children the codeine in it turns into too much morphine and slows breathing.',
watchFor: ['very sleepy or hard to wake', 'slow or shallow breathing'],
sources: ['mhra-codeine-children', 'mhra-codeine-cough-children', 'ema-codeine-children'], question: 'Is there a medicine without codeine that suits a child?',
clinical: 'Codeine under 12 years: not for pain (MHRA 2013, EMA) and contraindicated for cough (MHRA 2015): unpredictable CYP2D6 conversion to morphine, respiratory depression.', review: approved },
{ id: 'child-tramadol-under-12', match: { ingredient: 'tramadol' }, under: 12, tier: 'today',
why: '{a} isn\'t for children under 12: in some children tramadol slows breathing.',
watchFor: ['very sleepy or hard to wake', 'slow or shallow breathing'],
sources: ['label-tramadol'], question: 'Is there a medicine without tramadol that suits a child?',
clinical: 'Tramadol under 12 years: contraindicated (label, FDA 2017): ultra-rapid metabolism, respiratory depression.', review: approved },
{ id: 'child-promethazine-under-2', match: { ingredient: 'promethazine' }, under: 2, tier: 'today',
why: '{a} isn\'t for children under 2: promethazine can slow a young child\'s breathing dangerously.',
watchFor: ['very sleepy or hard to wake', 'slow or shallow breathing'],
sources: ['label-promethazine'], question: 'Is {a} right for a child of this age?',
clinical: 'Promethazine under 2 years: contraindicated (label boxed warning): fatal respiratory depression.', review: approved },
{ id: 'child-cough-cold-promethazine-under-6', match: { ingredient: 'promethazine' }, under: 6, tier: 'today', nameWords: ['cough', 'cold', 'colds', 'flu', 'catarrh'],
why: 'Cough and cold medicines like {a} aren\'t for children under 6.',
watchFor: ['very sleepy or hard to wake'],
sources: ['mhra-cough-cold-children'], question: 'What else can I give a child this age for a cough or cold?',
clinical: 'OTC cough and cold medicines with antihistamines (promethazine) not for under 6 years (MHRA 2009).', review: approved },
{ id: 'child-night-nurse-under-16', match: { product: 'night nurse' }, under: 16, tier: 'today',
why: '{a} isn\'t for children under 16.',
watchFor: ['very sleepy or hard to wake'],
sources: ['label-night-nurse'], question: 'What else can I give a child for a cold?',
clinical: 'Night Nurse (paracetamol, promethazine, dextromethorphan): do not take under 16 years (patient leaflet).', review: approved },
];
export interface MonitoringRule {
id: string;
match: Matcher;
test: string;
/** Lab result code it is satisfied by (see the documents/results catalogue). */
labCode: string | null;
/** How often, in days, for "due" when a dated result exists. */
everyDays: number | null;
why: string;
when: string;
sources: CheckSourceId[];
review: 'draft' | 'approved';
}
/** RS9: only guideline- or label-backed tests, each with its source. Never invented. */
export const MONITORING_RULES: MonitoringRule[] = [
{ id: 'monitor-warfarin-inr', match: { class: 'anticoagulant_vka' }, test: 'INR blood test', labCode: 'inr', everyDays: 84,
why: 'Checks that the blood-thinning dose is right.', when: 'As often as the clinic says, at least every 12 weeks when stable',
sources: ['chest-2012', 'nhs-warfarin'], review: approved },
{ id: 'monitor-metformin-kidneys', match: { ingredient: 'metformin' }, test: 'Kidney function (eGFR)', labCode: 'egfr', everyDays: 365,
why: 'The metformin dose depends on how well the kidneys work.', when: 'At least once a year',
sources: ['label-metformin'], review: approved },
{ id: 'monitor-metformin-b12', match: { ingredient: 'metformin' }, test: 'Vitamin B12', labCode: 'vitamin_b12', everyDays: null,
why: 'Long-term metformin can lower vitamin B12.', when: 'From time to time, especially with tingling or numb feet',
sources: ['ada-standards'], review: approved },
{ id: 'monitor-diabetes-hba1c', match: { class: 'biguanide' }, test: 'HbA1c', labCode: 'hba1c', everyDays: 182,
why: 'Shows the average blood sugar over the last few months.', when: 'Every 3 to 6 months',
sources: ['nice-ng28', 'ada-standards'], review: approved },
{ id: 'monitor-acei-kidneys-potassium', match: { class: 'acei' }, test: 'Kidney function and potassium', labCode: 'potassium', everyDays: 365,
why: 'These medicines can change kidney function and potassium.', when: 'At the start, after dose changes, then at least yearly',
sources: ['nice-cks-ace'], review: approved },
{ id: 'monitor-arb-kidneys-potassium', match: { class: 'arb' }, test: 'Kidney function and potassium', labCode: 'potassium', everyDays: 365,
why: 'These medicines can change kidney function and potassium.', when: 'At the start, after dose changes, then at least yearly',
sources: ['nice-cks-ace'], review: approved },
{ id: 'monitor-spironolactone-potassium', match: { class: 'k_sparing' }, test: 'Potassium and kidney function', labCode: 'potassium', everyDays: 182,
why: 'This medicine can raise potassium.', when: 'Regularly, as your doctor advises',
sources: ['nice-cks-ace'], review: approved },
{ id: 'monitor-bp-home', match: { class: 'calcium_channel_blocker' }, test: 'Home blood pressure', labCode: 'bp_systolic', everyDays: 30,
why: 'Shows whether the blood pressure medicine is working.', when: 'As your doctor advises; home readings help',
sources: ['nice-ng136'], review: approved },
{ id: 'monitor-statin-lipids', match: { class: 'statin' }, test: 'Cholesterol', labCode: 'ldl', everyDays: 365,
why: 'Shows whether the statin is working.', when: 'About 3 months after starting, then yearly',
sources: ['nice-lipids'], review: approved },
{ id: 'monitor-lithium-level', match: { ingredient: 'lithium' }, test: 'Lithium level, kidneys and thyroid', labCode: null, everyDays: null,
why: 'Lithium needs regular blood tests to stay at the right level.', when: 'As your mental health team advises',
sources: ['label-lithium'], review: approved },
{ id: 'monitor-methotrexate-bloods', match: { ingredient: 'methotrexate' }, test: 'Blood count, liver and kidney tests', labCode: null, everyDays: null,
why: 'Methotrexate needs regular blood tests.', when: 'As your specialist advises',
sources: ['label-methotrexate'], review: approved },
{ id: 'monitor-levothyroxine-tsh', match: { class: 'thyroid_hormone' }, test: 'Thyroid blood test (TSH)', labCode: 'tsh', everyDays: 365,
why: 'Checks that the thyroid dose is right.', when: 'After dose changes, then about yearly',
sources: ['label-levothyroxine'], review: approved },
];
/**
* A published threshold. `threshold` is a number, or the person's own target
* (when it is plausible, see src/domain/labs/targets.ts) with a fallback.
* `inclusive`: the threshold itself counts (>= or <=). A value printed as a
* bound (">8", "<15") is past the threshold only when every value it allows
* is: ">8" meets "INR 5 or more" and "<15" meets "eGFR below 30", but ">2"
* against "above a target of 3" can't be told, and is reported as not checked.
*/
export interface LabBound {
direction: 'above' | 'below';
threshold: number | { target: 'high' | 'low'; otherwise: number };
inclusive: boolean;
}
/** L4/L5 (H3): lab values that change what a medicine means. Thresholds only where a label or guideline sets one. */
export interface LabRule {
id: string;
/** Rules in a group are one ladder for one test: tried in table order, only the first that applies is reported. */
group: string;
/** Any medicine matching one of these. All of them are named in the finding. */
match: Matcher[];
/** Applies even with none of those medicines (a dangerous potassium is dangerous on its own, M-7); they are named when present. */
anyMedicine?: boolean;
/** The reason when no matching medicine is taken. */
whyAlone?: string;
labCode: string;
bound: LabBound;
tier: Tier;
/** The tier when the symptoms include bleeding or stroke signs (RS6). */
tierWithBleeding?: Tier;
/** "Call 112 now" is for a recent result: one older than this many days is acted on at `olderTier`. (Every rule: older than 14 days is today at most, older than 90 days isn't used.) */
recentDays?: number;
olderTier?: Tier;
/** Title template; the default depends on the tier. {a} medicines, {test} the test's name. */
title?: string;
why: string;
/**
* What to do. {action} is the tier's own action, {emergency} the emergency
* line, {a} the medicines, {possessive} "your" or "her". Default: the
* tier's action and "keep taking".
*/
actions?: string[];
/** false: the finding says to call before the next dose and the verdict leaves out "keep taking": the clinic decides the next dose. */
keepTaking?: boolean;
/** Sooner than its tier says: within a day or two. The verdict's headline says so (N23). */
soon?: boolean;
/** The help card's sign when this rule makes a finding get_help_now. */
sign?: string;
watchFor: string[];
sources: CheckSourceId[];
question: string;
clinical: string;
/** Extra medicine that must also be present (e.g. an NSAID for L5). */
alsoNeeds?: Matcher;
review: 'draft' | 'approved';
}
/** Medicines that raise potassium. */
const RAISES_POTASSIUM: Matcher[] = [{ class: 'acei' }, { class: 'arb' }, { class: 'k_sparing' }, { class: 'potassium_supplement' }];
const INR_TARGET = { target: 'high' as const, otherwise: 3 };
export const LAB_RULES: LabRule[] = [
// Metformin and kidney function (label).
{ id: 'lab-metformin-egfr-30', group: 'metformin-egfr', match: [{ ingredient: 'metformin' }], labCode: 'egfr',
bound: { direction: 'below', threshold: 30, inclusive: false }, tier: 'today', keepTaking: false,
why: 'The latest kidney result is below the level where the {a} label says not to use it.', watchFor: ['feeling very weak', 'fast breathing', 'stomach pain'],
sources: ['label-metformin'], question: 'What should happen with {a} given my kidney result?',
clinical: 'Metformin contraindicated at eGFR < 30 mL/min/1.73m2 (label).', review: approved },
{ id: 'lab-metformin-egfr-45', group: 'metformin-egfr', match: [{ ingredient: 'metformin' }], labCode: 'egfr',
bound: { direction: 'below', threshold: 45, inclusive: false }, tier: 'this_week',
why: 'The latest kidney result is in the range where the {a} label says to review the dose.', watchFor: [],
sources: ['label-metformin'], question: 'Is my {a} dose right for my kidney result?',
clinical: 'Metformin: reassess benefit/risk and dose at eGFR 30-45 (label).', review: approved },
// INR on a vitamin K antagonist: the higher it is, the sooner. Never a dosing instruction: the clinic decides.
{ id: 'lab-inr-8', group: 'inr', match: [{ class: 'anticoagulant_vka' }], labCode: 'inr',
bound: { direction: 'above', threshold: 8, inclusive: true }, tier: 'today', tierWithBleeding: 'get_help_now', keepTaking: false,
title: 'Call today: {a} and a very high INR',
why: 'The latest INR is 8 or more. The blood is much too thin, and the chance of serious bleeding is high.',
actions: ['Call {possessive} anticoagulation clinic or doctor today, before the next {a} dose.', 'If they can\'t be reached today, go to the nearest hospital today.',
'If there is bleeding that won\'t stop, blood in vomit or poo, or a sudden bad headache: {emergency}', 'Never change the {a} dose without the clinic.'],
watchFor: ['bleeding that won\'t stop', 'blood in vomit or poo', 'a sudden bad headache'],
sources: ['chest-2012', 'nhs-warfarin'], question: 'What should happen with {a} given this INR?',
clinical: 'INR >= 8: high bleeding risk; same-day contact with the anticoagulation clinic (dose withholding and vitamin K per protocol); emergency care if bleeding.', review: approved },
{ id: 'lab-inr-5', group: 'inr', match: [{ class: 'anticoagulant_vka' }], labCode: 'inr',
bound: { direction: 'above', threshold: 5, inclusive: true }, tier: 'today', tierWithBleeding: 'get_help_now', keepTaking: false,
why: 'The latest INR is 5 or more, well above the target, so the blood may be too thin.',
actions: ['Call {possessive} anticoagulation clinic or doctor today, before the next {a} dose.', 'Never change the {a} dose without the clinic.'],
watchFor: ['unusual bruising or bleeding', 'black or bloody poo', 'blood in pee'],
sources: ['chest-2012', 'nhs-warfarin'], question: 'What should happen with {a} given this INR?',
clinical: 'INR 5 to < 8: same-day contact with the anticoagulation clinic before the next dose.', review: approved },
{ id: 'lab-inr-high-nsaid', group: 'inr', match: [{ class: 'anticoagulant_vka' }], labCode: 'inr', alsoNeeds: { class: 'nsaid' },
bound: { direction: 'above', threshold: INR_TARGET, inclusive: false }, tier: 'today',
why: 'The latest INR is above the target while {b} is also being taken.', watchFor: ['unusual bruising or bleeding', 'black or bloody poo'],
actions: ['{action}', 'Never change the {a} dose without the clinic.'],
sources: ['chest-2012', 'nhs-warfarin'], question: 'What should I do about my INR and {b}?',
clinical: 'INR above target with concurrent NSAID: bleeding risk; contact anticoagulation clinic same day.', review: approved },
{ id: 'lab-inr-4-5', group: 'inr', match: [{ class: 'anticoagulant_vka' }], labCode: 'inr',
bound: { direction: 'above', threshold: 4.5, inclusive: true }, tier: 'this_week', soon: true, keepTaking: false,
why: 'The latest INR is 4.5 or more, above the target.', watchFor: ['unusual bruising or bleeding', 'black or bloody poo'],
actions: ['Ask {possessive} anticoagulation clinic about this result in the next day or two.', 'Never change the {a} dose without the clinic.'],
sources: ['chest-2012', 'nhs-warfarin'], question: 'What does this INR mean for my {a}?',
clinical: 'INR 4.5 to < 5: above any usual target; clinic review within a day or two.', review: approved },
{ id: 'lab-inr-high', group: 'inr', match: [{ class: 'anticoagulant_vka' }], labCode: 'inr',
bound: { direction: 'above', threshold: INR_TARGET, inclusive: false }, tier: 'this_week',
why: 'The latest INR is above the target.', watchFor: ['unusual bruising or bleeding'],
actions: ['{action}', 'Never change the {a} dose without the clinic.'],
sources: ['chest-2012', 'nhs-warfarin'], question: 'What does this INR mean for my {a}?',
clinical: 'INR above target range and below 5; clinic to review dose. Never change the dose without the clinic.', review: approved },
// Potassium on medicines that raise it (NICE CKS hyperkalaemia: 6.5 or more, emergency admission; 6.0 to 6.4, same day).
{ id: 'lab-potassium-very-high', group: 'potassium', match: RAISES_POTASSIUM, labCode: 'potassium',
bound: { direction: 'above', threshold: 6.5, inclusive: true }, tier: 'get_help_now', recentDays: 2, olderTier: 'today', keepTaking: false, anyMedicine: true,
sign: 'Very high potassium in the latest blood test',
why: 'The latest potassium result is very high. This can upset the heartbeat, and {a} can raise it further.',
whyAlone: 'The latest potassium result is very high. This can upset the heartbeat.',
actions: ['{action}'], watchFor: ['muscle weakness', 'an irregular heartbeat'],
sources: ['nice-cks-hyperkalaemia'], question: 'What should happen with {a} given my potassium?',
clinical: 'K+ >= 6.5 mmol/L on a medicine that raises potassium: emergency assessment (NICE CKS hyperkalaemia).', review: approved },
{ id: 'lab-potassium-high', group: 'potassium', match: RAISES_POTASSIUM, labCode: 'potassium',
bound: { direction: 'above', threshold: 6, inclusive: true }, tier: 'today', keepTaking: false, anyMedicine: true,
why: 'The latest potassium result is high, and {a} can raise it further.',
whyAlone: 'The latest potassium result is high. This can upset the heartbeat.',
actions: ['{action}'], watchFor: ['muscle weakness', 'an irregular heartbeat'],
sources: ['nice-cks-hyperkalaemia', 'nice-cks-ace'], question: 'What should happen with {a} given my potassium?',
clinical: 'K+ 6.0 to 6.4 mmol/L on a medicine that raises potassium: same-day advice (NICE CKS hyperkalaemia).', review: approved },
{ id: 'lab-potassium-raised', group: 'potassium', match: RAISES_POTASSIUM, labCode: 'potassium',
bound: { direction: 'above', threshold: 5.5, inclusive: true }, tier: 'this_week',
why: 'The latest potassium result is raised, and {a} can raise it further.',
watchFor: ['muscle weakness', 'an irregular heartbeat'],
sources: ['nice-cks-hyperkalaemia'], question: 'When should my potassium be checked again?',
clinical: 'K+ 5.5 to 5.9 mmol/L on a medicine that raises potassium: review and repeat (NICE CKS hyperkalaemia).', review: approved },
];
/**
* RS6: symptoms trigger "get help now", not medicine pairs.
*
* Each pattern is a regular expression, matched whole-word against the text
* after normalizeText(): lower case, no accents, apostrophes dropped ("can't"
* is "cant") and other punctuation turned into spaces. Patterns cover common
* spellings and the common Nigerian Pidgin forms ("I no fit breathe", "I dey
* vomit blood"). They over-match on purpose: a false "get help now" costs a
* call, a missed one can cost a life. Negations ("no chest pain") still match.
*
* Sign-off (signoff.ts): the signs and their examples in the review pack of 13
* October 2026 were approved with the rules; the patterns themselves weren't
* shown in it. Everything changed since waits for a top-up review: breathing
* slowly, a wound that won't stop bleeding, poison ivy and the other pattern
* changes, the self-harm sign's words moving to intent.ts, and the new sign
* "Feeling like they are dying".
*/
export interface RedFlag {
id: string; sign: string; patterns: string[];
/** For the pharmacist's review; never shown, never changes a result. */
notes?: string;
}
// Building blocks, kept as regular-expression source.
const BREATHE = 'breath(?:e|es|ed|ing|in)?';
/** LOW: breathing slowly on purpose, as advice or an exercise, not a sign: "breathe slowly to calm down", "slow breathing exercises", "when I'm anxious". */
const TO_CALM = '(?! (?:exercises?|techniques?|practice)(?![a-z0-9]))(?!(?: (?:in|out|and|deeply|through (?:my|your|her|his|the) (?:nose|mouth)))* (?:to|so|and|will|helps?) (?:help (?:me |you |her |him )?)?(?:calm|relax|settle|ease|stay calm|control|manage|get through)(?![a-z0-9]))(?! when (?:i|im|i am|i feel|i get|you|you feel|you get) (?:anxious|stressed|nervous|panicky|panicking|upset|worried)(?![a-z0-9]))';
const NOT_ABLE = '(?:cant|cannot|can not|couldnt|could not|unable to|not able to|no fit|no able to|no dey fit|hardly|struggl(?:e|es|ed|ing) to|fight(?:s|ing)? to|hard to|finding it hard to|difficult to)';
// Small words that can sit between a body part and what happened to it.
/** Ten or more of something, in figures or words. */
export const NUM10 = '(?:[1-9][0-9]+|ten|eleven|twelve|thirteen|fourteen|fifteen|sixteen|seventeen|eighteen|nineteen|twenty|thirty|forty|fifty|sixty|seventy|eighty|ninety|a hundred|hundred|a dozen|dozens?)';
/**
* M1: when or how often, after a count: "14 tablets a day", "12 tablets of prednisolone in the morning", "24 Coartem
* tablets over 3 days". A count with a schedule is a regimen, not too much at once. Not "tonight" or "now".
*/
export const SCHEDULE = '(?:(?:a|per|each|every) (?:day|week|night|morning|evening|afternoon)|daily|weekly|nightly|twice|thrice|(?:two|three|four|2|3|4) times|once (?:a|per|each|every)|in the (?:morning|evening|afternoon)|at (?:night|bedtime)|morning and (?:evening|night)|(?:over|for) (?:[0-9]+|two|three|four|five|six|seven|ten|fourteen|several|a few) (?:days|weeks|months)|(?:a|the|this|that|my|her|his|their|whole|full) course|in total|altogether|(?:a|per|each|every) dose|every (?:[0-9]+|four|six|eight|twelve) hours|as usual|as prescribed)';
/**
* M1: a word between a count and its schedule, in the same clause: never one that starts a new one ("I took 20
* paracetamol, I feel sick every morning" is not a schedule).
*/
export const SAME_CLAUSE = '(?: (?!(?:i|im|ive|id|she|he|shes|hes|they|we|you|it|its|and|but|then|so|because|cos|feel|feels|felt|am|is|are|was|were|been|now|still)(?![a-z0-9]))[a-z0-9]+)';
/** M1: a schedule just before the count ("Every morning she takes 12 tablets"). */
const SCHEDULE_BEFORE = '(?:(?:every|each) (?:day|week|night|morning|evening|afternoon)|daily|weekly|nightly|in the (?:morning|evening|afternoon)|at (?:night|bedtime))';
/** M1: fifty or more is never a regimen, schedule or not ("Can I take 100 paracetamol a day?"). */
const NUM50 = '(?:[5-9][0-9]|[1-9][0-9]{2,}|fifty|sixty|seventy|eighty|ninety|a hundred|hundred|hundreds|dozens)';
/**
* What a count is not: a dose or a time ("took 10 mg", "took 20 days"), or how far through a course ("14 of my 28"). M-4: "30 of
* my 10mg diazepam" is a count, not a course: the number after "of my" is never a strength.
*/
const NOT_A_COUNT = '(?! ?(?:mg|g|ml|mcg|ug|units?|iu|drops?|kg|minutes?|mins?|hours?|hrs?|days?|weeks?|months?|years?|am|pm|oclock)(?![a-z]))'
+ '(?! (?:[a-z]+ )?(?:of|out of) (?:my |her |his |their |the |your )?(?:[0-9]+(?![0-9.])(?! ?(?:mg|mgs|g|gm|grams?|milligrams?|mcg|ug|micrograms?|ml|mls)(?![a-z]))|course|pack|packet|box))';
/** LOW: for a long time, so not a sign that is new ("since the fall last year", "for years"). */
const LONG_STANDING = '(?:last (?:year|month)|(?:years|months) ago|for (?:years|months|a long time)|since (?:childhood|birth|(?:19|20)[0-9]{2}))';
/** Who a question is about, in the inverted forms "can't she breathe", "won't he wake" (H1). */
const SUBJECT = '(?:she|he|i|they|you|we|it|my [a-z]+|her [a-z]+|his [a-z]+|their [a-z]+|the baby|baby|mama|papa|mum|mom|dad|mummy|daddy)';
const CANT_INVERTED = '(?:cant|cannot|can not|couldnt|could not|wont|will not|isnt|is not|wasnt|was not|doesnt|does not)';
const FILLER = '(?:is|are|was|were|be|been|being|has|have|had|dey|don|got|get|gets|getting|look|looks|looked|looking|feel|feels|felt|feeling|seem|seems|seemed|start|starts|started|starting|begin|began|begun|keep|keeps|kept|went|go|gone|going|become|becomes|became|all|and|or|her|his|my|their|your|the|a|an|of|in|on|around|very|so|really|bit|little|too|up|to|now|suddenly|just|also)';
const FACE_PART = '(?:lip|lips|tongue|face|throat|mouth|eyelid|eyelids)';
const SWELL = '(?:swell|swells|swelled|swelling|swollen|swole|puff(?:s|ed|ing|y)?|blow(?:n|ing)? up)';
const VOMIT = '(?:vomit+(?:s|ed|ing)?|throw(?:s|ing)? up|threw up|puk(?:e|es|ed|ing)|cough(?:s|ed|ing)?|spit(?:s|ting)?|spat|bring(?:s|ing)? up|brought up)';
const NOT_BLOOD_TEST = '(?! (?:pressure|sugar|glucose|tests?|thinners?|counts?|levels?|results?|group|type))';
const POO = '(?:poo|poop|stool|stools|faeces|feces|shit|motion|motions)';
/** M2: speaking to a professional or a service is not a sign of a stroke ("can't talk to my doctor today"). */
const PROFESSIONAL = '(?:to|with) (?:my |her |his |their |your |our |the |a |an |any )?(?:doctor|doctors|gp|pharmacist|pharmacy|chemist|nurse|clinic|hospital|specialist|consultant|midwife|receptionist|dentist)';
const WONT = '(?:wont|will not|doesnt|does not|didnt|did not|cant|cannot|can not|isnt|is not|not|no gree|no dey|no wan|refuse to|refused to|wouldnt|would not)';
/**
* Negative context (N7): a sign named as something a medicine causes or
* prevents, or that happened long ago, isn't happening now. Kept narrow: "she
* just had a heart attack" still counts.
*/
const CAUSED = '(?<!(?:cause|causes|caused|causing|give|gives|giving|lead to|leads to|risk of|risks of|chance of|history of|prone to|side effects?(?: like| such as| of)?|prevent|prevents|preventing|avoid|avoiding|stop|stops|reduce|reduces|lower|lowers|protect against|protects against|protect from|protects from)(?: any| some| a| an| the| more| future| another)? )';
const LONG_AGO = '(?! (?:in (?:19|20)[0-9]{2}|(?:[0-9]+|a|one|two|three|four|five|six|seven|eight|nine|ten|many|several|some) (?:years?|months?|weeks?) ago|years? ago|last (?:year|month|week)|before))';
const HOURS = '(?:an? |over an? |more than an? |about an? |almost an? |nearly an? |half an? |(?:[0-9]+|two|three|four|five|six|several|many|some|a few|over [0-9]+) )?(?:hour|hours)';
export const RED_FLAGS: RedFlag[] = [
{ id: 'breathing', sign: 'Trouble breathing', patterns: [
`${CANT_INVERTED} ${SUBJECT} (?:breathe|breathing|catch (?:her |his |my |their |your )?breath|get (?:her |his |my |their |your )?breath)`,
`${NOT_ABLE} ${BREATHE}`,
`${CAUSED}(?:trouble|difficult(?:y|ies)|problems?|struggl(?:e|es|ing)|labou?red) (?:in |with |to )?(?:my |her |his |their |your |the )?${BREATHE}`,
`${CAUSED}${BREATHE}(?: ${FILLER}){0,2} (?:difficult(?:y|ies)|problems?|trouble|issues?)`,
`struggl(?:e|es|ed|ing) (?:for|to get|to catch) (?:a |her |his |my |their |your )?breath|short breath`,
`(?:stopped|stops|stop|not|isnt|is not|wasnt|was not|no dey|no longer) ${BREATHE}`,
// LOW: "Can the diazepam stop him breathing?"
`(?:stop|stops|stopped|stopping) (?:him|her|them|me|you|someone|people|a person|the baby|baby|(?:my|his|her|their|your) [a-z]+)(?: from)? ${BREATHE}`,
`short(?:ness)? (?:of|for) breath|out of breath|breathless(?:ness)?`,
`(?:cant|cannot|can not|couldnt|could not|no fit|struggl(?:e|es|ed|ing) to) (?:catch|get) (?:my |her |his |their |your )?breath`,
`gasp(?:s|ed|ing)?|chok(?:e|es|ed|ing)`,
`(?:slow|shallow|noisy|rattl(?:y|ing)|weak)(?: (?:and|or) (?:slow|shallow|noisy|weak))? ${BREATHE}${TO_CALM}`,
// LOW: "breathing slowly", a sign of too much opioid, in the same group as "breathing slow"; never "breathe slowly to calm down".
`${BREATHE}(?: ${FILLER}){0,3} (?:slow|slowly|shallow|shallowly|noisy|difficult|hard|laboured|labored|stopping|stopped)${TO_CALM}`,
// "breathing fast", but not after climbing the stairs or running.
`${BREATHE}(?: ${FILLER}){0,3} (?:fast|quick|quickly|rapid|rapidly)(?! (?:after|from|when|while) (?:climbing|running|walking|exercise|exercising|playing|sport|sports|the stairs|stairs|work|working|dancing|football))`,
// Hausa "numfashi", Igbo "iku ume" (breath) inside an English or Pidgin sentence.
`numfashi|iku ume`,
`breath (?:dey |don |no dey |no |is |was )?(?:cut|cutting|hard|short|tight|come|reach|catch)`,
// Pidgin: "my mother dey breathe anyhow" (L6).
`${BREATHE} (?:anyhow|somehow|any how)`,
`(?:lips?|face|tongue|skin|fingers|fingernails|nails)(?: ${FILLER}){0,3} (?:blue|bluish|grey|gray|purple)`,
`(?:blue|bluish|purple) (?:lips?|face|tongue|skin|fingers)|turn(?:s|ed|ing)? blue`,
] },
{ id: 'chest-pain', sign: 'Chest pain', patterns: [
`chest(?: ${FILLER}){0,3} (?:pain|pains|painful|hurt|hurts|hurting|ache|aches|aching|tight|tightness|pressure|heaviness|heavy|crushing|crushed|burning|sore)`,
`chest dey (?:pain|hurt|do (?:me|am|her|him))`,
`pain(?:s|ful)? (?:in|on|across|around|at) (?:my|her|his|their|the|your) chest`,
`tight(?:ness|ening)? (?:in |of |across )?(?:my |her |his |their |the |your )?chest`,
`${CAUSED}(?<!(?:after|since|previous|old|past|earlier) (?:a |an |the |her |his |my |their |your )?)heart attacks?${LONG_AGO}(?! (?:risk|prevention))`,
`pressure (?:in|on|across|around) (?:my|her|his|their|the|your) chest|chest pressure`,
] },
{ id: 'swelling', sign: 'Swelling of the face, lips, tongue or throat', patterns: [
// "Her lips feel tight" after an injection; lips or tongue that feel funny. Not "my mouth feels funny" or "my throat is
// funny when I swallow" (LOW).
`(?:lips?|tongue|throat|face)(?: [a-z]+){0,2} (?:feel|feels|felt|feeling|is|are|getting|go|dey) (?:tight|thick|swollen|big)`,
`(?:lips?|tongue)(?: [a-z]+){0,2} (?:feel|feels|felt|feeling|is|are|getting|go|dey) funny`,
`${FACE_PART}(?: ${FILLER}){0,4} ${SWELL}`,
`${SWELL}(?: ${FILLER}){0,4} ${FACE_PART}`,
`throat(?: ${FILLER}){0,2} (?:closing|closed|tight|tightening|blocked)`,
// A2b: "make my throat close up".
`throat(?: [a-z]+){0,2} (?:close|closes|closing|closed) up`,
`(?:cant|cannot|can not|unable to|no fit|not able to) swallow`,
] },
{ id: 'allergy', sign: 'Signs of a serious allergic reaction', patterns: [
`anaphyla[a-z]*`,
`(?:severe|bad|serious|big|strong) allergic reaction|allergic shock`,
`rash(?: [a-z]+){0,4} (?:faint|fainted|fainting|dizzy|collapse|collapsed)`,
`(?:faint|fainted|fainting)(?: [a-z]+){0,4} rash`,
] },
{ id: 'faint', sign: 'Fainting, collapse or hard to wake', patterns: [
// Pidgin: "she just dey look" (staring, not answering).
`just dey look(?! for| at| am| well| fine| better| good)|dey stare|dey look (?:anyhow|one kind)`,
`${CANT_INVERTED} ${SUBJECT} (?:wake|waking|wake up|waking up|be woken|respond|responding)`,
`faint(?:ed|ing|s)|(?:she|he|i|they|we|e|has|have|had|don|just|nearly|almost) faint`,
`pass(?:ed|es|ing)? out|black(?:ed|s|ing)? out|collaps(?:e|ed|es|ing)|uncon(?:s)?cious|unrespon(?:s)?ive`,
// R11-L3, R12-H1: not someone else's wish "to go to sleep and not wake up", which is the self-harm sign (intent.ts), never hard to
// wake; only those words are skipped: "not waking", "not responding", "not moving" and "no dey wake" are always hard to wake.
`(?<!(?:wants|wanted|wishes|wished) to (?:go to )?sleep and )not wake(?: up)?`,
`(?:isnt|is not|wasnt|was not|no dey|no|stopped|stops) (?:respond|responding|waking|wake|moving)(?: up)?|not (?:respond|responding|waking|moving)(?: up)?`,
`(?:cant|cannot|can not|couldnt|could not|wont|will not|hard to|difficult to|unable to|no fit) (?:wake|rouse|awaken)`,
`(?:very|so|too|really|extremely|unusually) (?:sleepy|drowsy)`,
`(?:cant|cannot|can not|couldnt|could not|no fit) (?:stay|keep) awake`,
] },
{ id: 'seizure', sign: 'A seizure (fit)', patterns: [
`s(?:ei|ie)zures?|seizing|convuls(?:e|es|ed|ing|ion|ions|ive)|fitting`,
// "Get fit" is exercise: only "fits", or "a fit" after had/having. M3: "she has fits" is a condition, not a fit
// happening, unless it says so ("he has fits again today").
`(?:had|having|started|starting) (?:a |an |another )?fits?|(?:has|have) (?:a |an |another )fit|(?:get|gets|got|getting) (?:fits|a fit|another fit)`,
`(?:has|have) fits(?: [a-z]+){0,2} (?:again|now|today|tonight|this morning|since|non stop|nonstop|all day)(?![a-z0-9])`,
`epilep(?:tic|sy) (?:fit|attack|seizure)`,
`(?:dey|don|is|was|keeps?|kept|started|start) jerk(?:ing|s)?`,
`body (?:dey|is|was|keeps?|kept|started|start|don start) (?:shake|shaking|jerk|jerking)`,
`(?:she|he|e|mama|mummy|papa|daddy|baby|pikin|child|di child|di pikin) (?:dey|don dey|don start) (?:shake|jerk)`,
`shaking and jerking|jerking and shaking|(?:shaking|jerking|twitching) (?:all over|uncontrollably|non stop|nonstop)`,
// M3: "her fits" only when they are happening ("her fits started again", "his fits won't stop"), or when
// something is causing them (A2: "Are the tablets causing his fits?"); not "Does Epilim stop her fits?".
`(?:his|her|my|their|your|the) fits(?: [a-z]+){0,2} (?:started|start|starting|have started|has started|are back|is back|came back|come back|coming back|wont stop|will not stop|dont stop|keep coming|keeps coming|again|now|today|tonight|this morning|happening)(?![a-z0-9])`,
`(?:causing|cause|causes|caused|giving|gives|give|bringing on|bring on|triggering|trigger|triggers|triggered) (?:his|her|my|their|the) fits`,
// LOW: "Can the tramadol make my mum have fits?", "give him fits".
`(?:make|makes|making|made|cause|causes|causing|caused|give|gives|giving|gave) (?:him|her|them|me|you|someone|people|a person|the baby|baby|(?:my|his|her|their|your) [a-z]+)(?: (?:have|having|get|getting|to have))? (?:a |an )?fits?`,
// M-b: several fits ("has had three fits since lunch"), or one that lasted 5 minutes or more.
`(?:had|has had|have had|having)(?: about| around| like| over| more than)? (?:[2-9]|[1-9][0-9]+|two|three|four|five|six|seven|eight|nine|ten|several|many|multiple|a few|some|more) (?:fits|seizures|convulsions)`,
`fits? (?:lasted|lasting|lasts|went on|has lasted|has gone on|have lasted|was|were)(?: for)?(?: about| around| over| more than| nearly| almost| at least)? (?:(?:[5-9]|[1-9][0-9]+|five|six|seven|eight|nine|ten|fifteen|twenty|thirty|forty|several|many) (?:minutes|mins)|half an hour|an hour|hours)`,
// Pidgin: "dey jijiji and foam for mouth".
`jijiji|jiji ji|foam(?:s|ed|ing)? (?:for|from|at|in) (?:the |her |his |my |him )?mouth|mouth(?: [a-z]+){0,2} (?:dey )?foam(?:s|ed|ing)?`,
] },
{ id: 'bleeding', sign: 'Bleeding that won\'t stop, or blood in vomit or poo', patterns: [
`${VOMIT}(?: (?:some|a|lot|lots|of|fresh|bright|red|dark|little|bit|small|amount|up|out|dey|is|was))* blood${NOT_BLOOD_TEST}`,
`blood(?:y)? (?:in|inside|for) (?:my |her |his |their |the |your )?(?:(?:pee|urine|wee) (?:or|and) )?(?:vomit|sick|${POO})`,
`pass(?:ing|ed|es)? blood${NOT_BLOOD_TEST}`,
`bloody (?:vomit|sick|diarrhoea|diarrhea|${POO})`,
`black (?:tarry |sticky |runny )?${POO}`,
`${POO}(?: ${FILLER}| turn| turned| turning){0,4} (?:black|tarry|bloody)`,
`tarry ${POO}|coffee grounds?`,
`(?:bleeding|blood|nosebleed|nose bleed|bleed)(?: [a-z]+){0,5} ${WONT} (?:stop|stopping|stopped)`,
// Not "bled a few hours ago but it stopped" (L6).
`(?:bleeding|bleed|bled|nosebleed|nose bleed)(?: (?!stopped|stops|stop|ended|ends|finished|eased|settled)[a-z]+){0,4} (?:for |since |over )?${HOURS}(?! ago)(?!(?: [a-z]+){0,2} (?:but|and|then) (?:it |that |the bleeding |bleeding )?(?:has |had )?(?:stopped|ended|finished|settled))`,
// Bleeding that stops and starts again (L6): "bleeding stopped but started again for hours".
`(?:bleeding|bleed|bled|nosebleed|nose bleed)(?: [a-z]+){0,4} (?:started|starts|starting|came|comes|keeps? (?:coming|starting)) (?:back|again)`,
`(?:vomit|sick|spit|sputum|phlegm|${POO})(?: [a-z]+){0,2} (?:has|have|had|got|with|contains?|containing|dey get|get|full of|mixed with) (?:some |a lot of |lots of |fresh )?blood${NOT_BLOOD_TEST}`,
`(?:cant|cannot|can not|couldnt|could not|unable to|no fit) stop (?:the |her |his |my |their |your )?(?:bleeding|blood|nosebleed)`,
// R9: "it won't stop bleeding" (the cut, the gum), so a cut or a burn that isn't self-harm (R9-M5) still gets this sign.
`${WONT} stop (?:the |her |his |my |their |your )?(?:bleeding|blood|nosebleed)`,
`(?:heavy|heavily|severe|severely|serious|massive|a lot of|lots of|plenty|too much) (?:bleeding|blood loss)`,
`bleeding (?:heavily|a lot|badly|plenty|non stop|nonstop|everywhere|profusely)`,
`losing (?:a lot of |lots of |too much )?blood${NOT_BLOOD_TEST}`,
`blood (?:is |dey |keeps |kept )?(?:pouring|gushing|pour|gush)`,
] },
{ id: 'stroke', sign: 'Signs of a stroke', patterns: [
// Inverted, as for "can't speak" (M2): anyone, the writer included, but not talking to someone, in a language or on the phone.
`${CANT_INVERTED} ${SUBJECT} (?:speak|talk)(?! (?:to|with|english|yoruba|igbo|hausa|pidgin|french|arabic|the language|their language|our language|much|about|on the phone|over the phone|for long))`,
`${CANT_INVERTED} ${SUBJECT} (?:move|lift|raise|feel|use)(?: (?:her|his|my|their|your|the|one|left|right|both))* (?:arm|arms|leg|legs|hand|hands|face|side)`,
`(?:face|mouth|smile|lip)(?: ${FILLER}){0,3} (?:droop|droops|drooping|drooped|dropped|dropping|fallen|falling|twisted|twist|twisting|crooked|lopsided|slanted)`,
`(?:droop|drooping|drooped|droopy|crooked|twisted) (?:face|mouth|smile)`,
`slurr(?:ed|ing)(?: [a-z]+)? (?:speech|words|talking|voice)|slurring`,
`(?:speech|talking|words|voice)(?: ${FILLER}){0,3} (?:slurred|slurring|garbled|jumbled|strange|weird|funny|unclear)`,
// M2: anyone, the writer included, who can't speak or talk (but not in a language, on the phone, much or for long).
`(?:cant|cannot|can not|couldnt|could not|unable to|no fit|not able to) (?:speak|talk)(?! (?:to|with|english|yoruba|igbo|hausa|pidgin|french|arabic|the language|their language|our language|much|about|on the phone|over the phone|for long))`,
// M2: someone else who can't talk to the people around them; not the writer, and not about reaching a professional.
`(?<!\\bi )(?:cant|cannot|can not|couldnt|could not|unable to|no fit|not able to) (?:speak|talk) (?!${PROFESSIONAL})(?:to|with)`,
`(?:no|not|no dey|no fit|stopped|stop) (?:talk|talking|speak|speaking|answer|answering)(?: again| anymore| any more)(?! ${PROFESSIONAL})`,
// "My papa hand no dey move"; "her speech sounds drunk".
// Not for a long time ("My leg no dey move well since the fall last year"; LOW).
`(?:hand|hands|arm|arms|leg|legs|foot|side|face)(?: [a-z]+){0,1} no (?:dey |fit )?(?:move|work|lift|raise)(?!(?: [a-z0-9]+){0,6} ${LONG_STANDING}(?![a-z0-9]))`,
`(?:speech|talking|words)(?: [a-z]+){0,2} (?:sounds?|sounding|seems?) (?:drunk|slurred|garbled|strange|funny|weird)`,
// A voice that sounds funny is often hoarseness ("since I started the inhaler"; LOW).
`voice(?: [a-z]+){0,2} (?:sounds?|sounding|seems?) (?:drunk|slurred|garbled)`,
`sudden(?:ly)? (?:weak|weakness|numb|numbness|heaviness|heavy|paraly[sz]ed)(?: [a-z]+){0,4} (?:arm|arms|leg|legs|hand|hands|face|side)`,
`(?:cant|cannot|can not|couldnt|could not|unable to|no fit|not able to|can no longer|could no longer|no longer able to) (?:lift|raise|move|feel|use)(?: (?:her|his|my|their|your|the|one|left|right|both|an|a))* (?:arm|arms|leg|legs|hand|hands|side)`,
`(?:weak|weakness|numb|numbness|paralys[a-z]*|paralyz[a-z]*)(?: [a-z]+){0,4} (?:one|left|right) side`,
`(?:one|left|right) side(?: of (?:the|her|his|my|their|your))? (?:body|face)(?: ${FILLER}){0,3} (?:weak|numb|droop[a-z]*|paralys[a-z]*|paralyz[a-z]*|dead|heavy)`,
`sudden(?:ly)? confus(?:ed|ion)|sudden (?:severe |bad |very bad |terrible |awful )?headache|worst headache`,
`having (?:a )?stroke`,
] },
// M-1: feeling like they are dying ("I feel like I'm dying", "I'm scared I'm going to die"), never labelled self-harm. Its words are
// in intent.ts, the one list for check symptoms and Ask.
{ id: 'dying', sign: 'Feeling like they are dying', patterns: [],
notes: 'Pharmacist to confirm (M-1): get help now, or call the doctor today? "I feel like I\'m dying" can be panic or a side effect, '
+ 'as well as an emergency.' },
{ id: 'overdose', sign: 'Too much medicine, or poison, taken', patterns: [
`overdos(?:e|ed|es|ing)`,
`(?:took|taken|swallowed|ate|eaten|drank|drunk|don take|don swallow|don chop)(?: (?:her|his|my|their|the|a|of|all|some))* (?:too many|too much|plenty|the whole|whole|a whole|entire|all)(?: (?:her|his|my|their|the|of))* (?:tablets?|pills?|medicines?|drugs?|capsules?|bottle|pack|packet|box|strip|doses?)`,
`(?:took|taken|swallowed) too (?:many|much)`,
// Ten or more of something, but not a dose ("took 10 mg") or a time ("took 20 days").
// "Taken 14 of my 28 Coartem" is how far through a course someone is (N7).
// M1: not with a schedule ("took 12 tablets a day for a week").
// LOW: "took 10x paracetamol".
`(?:took|taken|swallowed|ate|eaten|drank|drunk|don take|don swallow)(?: (?:about|around|over|like|almost|nearly|up to|maybe))? ${NUM10}x?${NOT_A_COUNT}(?!(?: [a-z]+){0,3} so far)(?!${SAME_CLAUSE}{0,4} ${SCHEDULE}(?![a-z0-9]))`,
`(?:took|taken|swallowed|ate|eaten|drank|drunk|don take|don swallow|don chop)(?: (?:about|around|over|like|almost|nearly|maybe))? (?:[2-9]|[1-9][0-9]+|two|three|four|five|six|seven|eight|nine|ten|several|many|a few|some) (?:full )?(?:packs?|packets?|boxes?|bottles?|strips?|cards?|blisters?|sachets?|tubes?)`,
// A1: asked as a question, now or later ("Can I take 30 paracetamol?", "Is it ok to take the whole packet?",
// "What happens if I take all my tablets at once?", "How many would it take to kill me?"). R10-M4: never "at the same time",
// which asks how to take a regimen.
`(?:take|takes|taking|swallow|swallows|swallowing|give|gives|giving|have|eat|use)(?: (?:her|his|my|their|the|a|of|some|them|it|these|those))* (?:the whole|whole|a whole|the entire|entire|the rest of|rest of)(?: (?:her|his|my|their|the|of|these|those|them))* (?:tablets?|pills?|medicines?|medication|meds|drugs?|capsules?|bottles?|packs?|packets?|box|boxes|strips?|doses?)`,
`(?:take|takes|taking|took|taken|swallow|swallows|swallowing|swallowed|give|gives|giving|gave)(?: (?:her|his|my|their|the|of))* (?:all|them all|it all|all of them|all of it)(?: (?:of )?(?:her|his|my|their|the|these|those|them))?(?: (?:tablets?|pills?|medicines?|medication|meds|drugs?|capsules?))? (?:at once|all at once|in one go|at one time|at a go)`,
// M1: ten or more with no schedule ("Can I take 30 paracetamol?"); with one it is a regimen ("I take 14 tablets a
// day"). Not tablets someone has left ("I have 30 tablets left"), or different medicines ("15 different tablets").
`(?<!${SCHEDULE_BEFORE}(?: [a-z]+){0,2} )(?:take|takes|taking|swallow|swallows|swallowing|give|gives|giving|have)(?: (?:about|around|over|like|almost|nearly|up to|maybe|another))? ${NUM10}${NOT_A_COUNT}(?! (?:different|separate|kinds? of|types? of|sorts? of|medicines|medications)(?![a-z]))(?!${SAME_CLAUSE}{0,4} ${SCHEDULE}(?![a-z0-9]))(?!(?: [a-z]+){0,3} (?:left|remaining|spare|in (?:the|my|her|his|a) (?:pack|packet|box|bottle|strip|house|cupboard))(?![a-z]))`,
// ...always with "at once" or "in one go", and fifty or more, schedule or not.
`(?:take|takes|taking|took|taken|swallow|swallows|swallowing|swallowed|give|gives|giving|gave|have)(?: (?:about|around|over|like|almost|nearly|up to|maybe|another))? ${NUM10}${NOT_A_COUNT}(?: [a-z]+){0,4} (?:at once|all at once|in one go|at one time|at a go)(?![a-z0-9])`,
`(?:take|takes|taking|took|taken|swallow|swallows|swallowing|swallowed|give|gives|giving|gave)(?: (?:about|around|over|like|almost|nearly|up to|maybe|another))? ${NUM50}${NOT_A_COUNT}(?!(?: [a-z]+){0,3} (?:left|remaining|spare)(?![a-z]))`,
`(?:take|takes|taking|swallow|swallows|swallowing|give|gives|giving)(?: (?:about|around|over|like|almost|nearly|maybe))? (?:[2-9]|[1-9][0-9]+|two|three|four|five|six|seven|eight|nine|ten|several|many|a few) (?:full )?(?:packs?|packets?|boxes?|bottles?|strips?|cards?|blisters?|sachets?)`,
// M-1: "how many ... to kill", Pidgin "go kill person", "how many before she dies": an amount.
`how (?:many|much)(?: [a-z0-9]+){0,8} (?:(?:to|would|will|could|can|go|fit) (?:kill|die|end it)|(?:before|until|till) (?:i|she|he|they|you|we|someone|somebody|a person|person) (?:die|dies|is dead))|(?:lethal|fatal|deadly|killing) (?:dose|doses|amount|number)|enough (?:of [a-z]+ )?to (?:kill|die|end it)`,
// "Would 20 tablets of amlodipine harm me?": a number or "all" with harm (A1). Not "Will the injection hurt me?".
`(?:${NUM10}|all|whole|many|lots|packets?|packs?)(?: [a-z]+){0,6} (?:harm|hurt|damage|poison) (?:me|myself|her|him|them)`,
`(?<!food )poison(?:ed|ing)?(?! (?:ivy|oak|sumac)(?![a-z]))`,
`(?:drank|drunk|swallowed|took|taken|don drink|don take|don swallow) (?:some )?(?:bleach|kerosene|insecticide|sniper|gam+alin|otapiapia|rat poison|detergent|acid|izal)`,
// A1b: now or later ("I go drink sniper").
`(?:drink|drinks|drinking|go drink|wan drink|will drink|want to drink|swallow|swallows|swallowing|go swallow|wan swallow) (?:some |the |dis |di |this |that )?(?:bleach|kerosene|insecticide|sniper|gam+alin|otapiapia|rat poison|detergent|izal|hypo)`,
`(?:take|takes|taking|go take|wan take|will take|want to take) (?:some |the |dis |di |this |that )?(?:bleach|kerosene|insecticide|sniper|gam+alin|otapiapia|rat poison|izal)`,
] },
// The words of intent (A1b, A1c, M1: "sleep forever", "on purpose", "mutu", "a lot of tablets"...) are in intent.ts, the one list
// for check symptoms and Ask (M2): they add the self-harm and overdose signs.
// Its words, wishes, plans, methods and amounts are in intent.ts, the one list for check symptoms and Ask (review 7).
{ id: 'self-harm', sign: 'Thoughts of suicide or self-harm', patterns: [] },
];