marmar-context / src / domain / checks / signoff.ts
signoff.ts
Raw
import { ALLERGY_RULES, CHILD_RULES, INTERACTION_RULES, LAB_RULES, MONITORING_RULES, OLDER_ADULT_RULES } from './rules.js';

/**
 * The clinical sign-off record of the check, next to the rules it covers.
 *
 * A sign-off approves exactly what the pharmacist was shown, and nothing made
 * after it. A rule in rules.ts is `review: 'approved'` only when its clinical
 * fields and the wording it gives are what a sign-off approved: APPROVED_CONTENT
 * keeps a hash of that content for each rule a sign-off approved, and
 * tests/clinical-signoff.test.ts fails when an approved rule no longer matches
 * it (the test says what to do instead). Anything
 * changed after the last sign-off waits for the next one (AWAITING_TOP_UP),
 * and while anything waits the check is never reported as signed off
 * (CHECK_SIGNED_OFF): not in a check's `rulesReviewed`, not in the quick list,
 * not on the admin rollout.
 */

export interface ClinicalSignOff {
  /** The day the sign-off was recorded here, which is also its id. */
  id: string;
  /** Who signed off. */
  by: string;
  /** Exactly what was reviewed. */
  reviewed: {
    what: string;
    /** The versions and commit the reviewed material was built from. */
    rulesVersion: string;
    engineVersion: string;
    commit: string;
    /** sha256 of the review pack's rules data (the JSON in its rules-data block), as published. */
    dataSha256: string;
  };
  /** How the approval was given, and how it reached us. */
  how: string;
  /** RULES_VERSION from this sign-off on. */
  rulesVersion: string;
  /** sha256 of this sign-off's APPROVED_CONTENT (stable JSON): what a sign-off approved is never edited after it is recorded. */
  approvedContentSha256: string;
}

/** Every clinical sign-off of the check, oldest first. */
export const CLINICAL_SIGN_OFFS: readonly ClinicalSignOff[] = [
  {
    id: '2026-10-15',
    by: 'Jamila Musa, PharmD',
    reviewed: {
      what: 'The review pack of 13 October 2026: every rule of the six rule tables with the wording the engine gave it for an example, the '
        + 'catalogue of medicines and products, the emergency signs with examples, the limits for results and targets, and 14 decisions.',
      rulesVersion: 'draft-2026-10-14',
      engineVersion: '2.1.0',
      commit: '82db1bc',
      dataSha256: '0c6d7f198506e4f4f066bda058ca67735092f03a16d39b068d1f0897eb0c9455',
    },
    how: 'Approved as written, as reported by the owner on 15 October 2026. The review page saved no answers, so there is no answer per rule.',
    rulesVersion: '2026-10-15',
    approvedContentSha256: 'e7c6ab4ac878f971d3ceef84883e026b9c7fa577c6c8284076578e8ff2dcde27',
  },
];

/**
 * What changed after the last sign-off and waits for a top-up review. None of
 * it is covered by a sign-off, so it keeps the check from being reported as
 * signed off. Empty it only when a sign-off covers all of it.
 */
export const AWAITING_TOP_UP: readonly string[] = [
  'Engine 2.2.0 dose checks: combinations checked one medicine at a time by their strength chips, one finding over several maximums, '
    + 'and the sentence that paracetamol can harm the liver before any signs appear.',
  'Three new reasons a dose could not be checked, with their lines: strength_unreadable, strength_not_per_ml and dose_unit_unknown.',
  'The source cited for each maximum (maxSources on 11 catalogue medicines, 7 new sources) and the opioid signs on dose findings.',
  'The danger screen shared by the symptoms box and Ask: the words of intent, the changed emergency signs, the new sign "Feeling like '
    + 'they are dying" and the line for words we could not be sure of.',
  'Too much paracetamol in the symptoms box and Ask: totals in mg, one rolling 24 hours, and amounts at clock times.',
  'Titles cut at a whole word and verdict sublines at a whole sentence.',
  'Medicine facts: what a medicine is for (usedFor), label maximums while a medicine is added, and the strength chips.',
  'Engine 2.2.1 dose maximum wording: entered amounts compared with general adult catalogue maxima, without claiming the person\'s '
    + 'label or a usual dose range was checked; partial combinations name the checked ingredients and retain a dose-appropriateness caveat. '
    + 'Engine 2.2.2 makes unassessed clinical context an incomplete verdict, scopes no-warning headlines to the limited checks, '
    + 'preserves uncertain health answers, and removes blanket continue-taking wording whenever the assessment is incomplete.',
];

/** Every rule of the six tables with a review field. */
export const REVIEWED_RULES = [...INTERACTION_RULES, ...ALLERGY_RULES, ...OLDER_ADULT_RULES, ...CHILD_RULES, ...MONITORING_RULES, ...LAB_RULES];

/** Signed off only when every rule is approved and nothing waits for a top-up review. */
export const CHECK_SIGNED_OFF: boolean = REVIEWED_RULES.every((rule) => rule.review === 'approved') && AWAITING_TOP_UP.length === 0;

/**
 * What each sign-off approved, by sign-off id: each rule's content hash (its
 * clinical fields and the wording it gives, tests/support/rule-content.ts).
 * Append only: a new sign-off adds its own group. A rule is checked against the
 * latest sign-off that approved it, so a rule set back to draft keeps its
 * entry here as the record of what was approved.
 */
export const APPROVED_CONTENT: Readonly<Record<string, Readonly<Record<string, string>>>> = {
  // The review pack of 13 October 2026: all 105 rules, unchanged since (rules draft-2026-10-14, engine 2.1.0, commit 82db1bc).
  '2026-10-15': {
    // Interactions
    'vka-nsaid': 'e2059d8552773b4cec5fd54c5728098672464298cec2c3fac147d9557b09491b',
    'doac-nsaid': 'cac4da45524a69be6c9b9c21fbaedbf889f14bbb48a7bbd6da8d0fce8ed620c2',
    'vka-aspirin-dose-unknown': 'e49fc8b16a48b67aa439b7a2464645e743475f1a4b7603dec88284be58b8d7b0',
    'doac-aspirin-dose-unknown': '98ad5f361b6fb255d01694b27dca75ce6702f06383e182cf2d4a9aa0c2cb794f',
    'anticoagulant-antiplatelet': '3363b4ae73960462e2dcc4646122d2370bc5dcf3e0b4e8f48fb7de9ea8aaf450',
    'doac-antiplatelet': 'f71c39c3610a7434a8882cf3534f61bec06b03d24a48d209db892c531ffc9a77',
    'vka-metronidazole': '5a4f8ed490299985cf68cd634ea91f4ee015f6e5976ce54ce78cdececcd29683',
    'vka-cotrimoxazole': 'ece5cc3444456f70347434c59d3b557235a08fdddbe8dc59b36fa83f7040a8df',
    'vka-azole': '6f6d1ceaa88bf5f540614613c81c65f3a970ccd99d337182b4cb187e135dfa73',
    'vka-fluoroquinolone': '7208865dcf4b243fba307dea33be57c548a9a154a6ebe74aac5352a0d76d18ca',
    'vka-macrolide': '9bd5d6f3cec9929cb3a7b8f4c39d9fe4b2432eb840e555ab7b3b997b21542450',
    'vka-inducer': '53c08c0e1f811704a5085e8eee5ecd517bcb4a3c62422d8b08fb85f7dd2f2d61',
    'vka-amiodarone': '530fa7a49ef4cfa03d910a170aa94f515656c0a68a6ca567b9f81d3755274f27',
    'nsaid-nsaid': '9088dd550fd00bd2ebdd6ec4aeccf45755b7803855765a35d8cb4b009f452945',
    'ssri-nsaid': '6c3f0bfeae318b1b883c0525cc00d81d4b4178fcff77c63cf6a371c0cb640983',
    'ssri-anticoagulant': '8fb2ecd9f49c58c017cd78a3ad9c03f36f0f26cf42b65335b60c49e004ce7546',
    'herbal-bleeding-anticoagulant': '1b267e53615045399efeb761d055816399ef8956133909eef5d85b8d2a177658',
    'herbal-bleeding-doac': 'c8cef9c0566f21912cb7f12609b2cdf810b09621cdaf3609193cce6a84a46c3e',
    'st-johns-wort-anticoagulant': 'c408d7b4d1331b320622e457b82a714f4d8dcfc86a0e6727e2260ede6dab9b65',
    'st-johns-wort-ssri': '8e0369020d67af3b56aae670bfa95593b26920296c023a3c1259d581e16199c3',
    'acei-arb': '3da4049b01b54cd7aefc7ca02b61b0a7a40af2773045bc57c7ad6bbd420de1b3',
    'raas-k-sparing': '12b321e37dfe52d4fce17f6da4e4575f31c64bcfca3edd1fae1cae4703ebd878',
    'arb-k-sparing': '0039d3d98f491b1acb3c36a5cc518e1bacb166a8758de9c93b9d1abf289cdc03',
    'raas-potassium': 'bea3dadc49428fd5a7f3095ee1f1a2648e90543a217f274b022f2a71a81642f8',
    'arb-potassium': 'd0a90cd7355271fe0c1ff4cc7a5cfe66af1ecd89fe2b6f6fbc5b836b3f576fc6',
    'nsaid-acei': 'bd5c1038119e54a16064cf95bf475dcbdc43f47657c30968fd0cfa9e31fd7477',
    'nsaid-arb': 'eed450334936192736cee9e5c8ffaa3f0bab498129a852ed4e509f82f344c86c',
    'simvastatin-strong-cyp3a4': '060faf72c145830197a026612964df8751cd6261b4545340263b0d7f6440a032',
    'atorvastatin-strong-cyp3a4': '54be6d327c5cf1879a479f6d5a0125d75fc71c9fb34a42f9b04fc3154fb4ec91',
    'simvastatin-amiodarone': '8729d26065cd60cdaeccdf4b45537f9a5e0bb9c6eab6b9f9e7913dbac9fa61c1',
    'digoxin-macrolide': '7c052c74640a95ea17dff43937a354c9af80254c8c9a389d2336bea409b778d6',
    'digoxin-amiodarone': '363ce9d612bfe38ed3488c5060cc45672ca7a6684b9548201d5f7719e3463b48',
    'pde5-nitrate': '16d592052d819f77787838d4b2c2951733790a0832a1689126cff655be273a12',
    'opioid-benzodiazepine': 'b66798f600bf5e9c0fd3e7b6bdd81343cd5c00303c067a3d9e8c884e35658ddc',
    'tramadol-ssri': 'cfa31b25fa60860f4b8390056aa4833ba8801c11f583439cc45d19173d49d83f',
    'tramadol-tca': 'c92e3be5a7475d8350423223899053fa08dad402673113e2baf1b6923cc53491',
    'methotrexate-trimethoprim': '1d3c4413e18045b9907ff4f864ae01adfafe002642813b6a604390547902c6c1',
    'methotrexate-nsaid': '2ca0cbff522c072ac22ec846dc1fb41aef4f8205588ad925fbf4cf25950928bb',
    'allopurinol-azathioprine': 'e6850f2e4707f6eb65273b1dc3b4b08fdb2c1f23c04db81a1041f1779b6c95a6',
    'lithium-nsaid': 'deead60ca42a1979fb5bee0a79633c4f648e9bea4b5fe4fe761d7cf1935d4558',
    'lithium-acei': 'f7c10259b1ea74c309f3471ac3bbb6b32ff553aa2e28f129e276702227e6f6a6',
    'lithium-arb': '9b5225b7bb367d94a0c4309c9222f18cceb70aca659f6e937e89a3562d2de45d',
    'lithium-thiazide': '105fae6cb34bd9360f1e6b7e1c0c80da8bbc1dd418736939f242e28d4e078487',
    'sulfonylurea-cotrimoxazole': '10be92951bda647cda82dc536d13fd9e9a3c56a7668b385c4b6d7c89aa0d6a38',
    'sulfonylurea-fluconazole': 'b488cb48a16ee73bedcbed8544703af80c4437217951a96eae90d18496146db1',
    'clopidogrel-omeprazole': '93bc4effdac717bed7cf6044e408dededbb39981acbbdf3e03ced04e4c4378fe',
    'clopidogrel-esomeprazole': 'ceefeefad2cb1d83c60f6ef96d161ed791393f9ec9e2018aac8b0fa2359ac9e9',
    'dolutegravir-rifampicin': '4b4d621ccfbee25c38b6cc6d27fa95077c763c00f89fc1791b5ad1795d17182f',
    'dolutegravir-cations': '099793f047db3d3b8e7b8a38b2538b6240a9d3958b39778e4b941537703c4056',
    'lumefantrine-efavirenz': '57fddbfb62edb1b6d041df1397d7bd07a45f9a35eadc6423108fc1d068dc010a',
    'apixaban-strong-inducer': '460e21316eefba0a53a2f784a523487c50ee0b236feb1e94051cf8b5c8e27773',
    'apixaban-st-johns-wort': '5a03e32c67c2fb725bd740921c1485afe75ff75838e9ae95b455f5318179d05f',
    'rivaroxaban-strong-inducer': 'cf52e2c8a34dd0dbaf12a798f8f9f077b978b4a3fd87c31f49e3861547bf5163',
    'rivaroxaban-st-johns-wort': '019be5d356fe8bc04ea180ca7a4920f6b811c3ef757fa9f88eae751e2efc6550',
    'lumefantrine-strong-inducer': 'a1e103b8a078ed75826eac48f537ff83792bef70a9d4293b398333f44f64e0e1',
    'lumefantrine-st-johns-wort': '469eef82b9e51279d1b03c9dae6acc490338d4aa482d3370baf2265370cef1cc',
    'amodiaquine-efavirenz': '45da17521884187b928b774a499d7755b6ef368d770f361b34d9fa247fd37f18',
    'dolutegravir-phenytoin': '4051727a78e2f4129a89b12eb56e2021943f499e9d5c506f89f197f3c309a03d',
    'dolutegravir-st-johns-wort': '23b069fca89ccccc035b44c5e02fecc4872d1ed824937938ab2495ad1a67dae9',
    'dolutegravir-carbamazepine': '1a9900aadf55166ba23ccb62d12cd92766beba7d9a98c292d57384798147dd42',
    'nevirapine-rifampicin': '60a283d1e7c80f155a7fd36abb3aa83c86b833a935327bd60b0016937775145b',
    'simvastatin-amlodipine': '97e0436a92fd1608c1206d9f1476bf1aee44419d94dcc011f1c369d69e53a715',
    'digoxin-loop-diuretic': '7ad43fba9e390af1c9f28595858210a41a0365d3a19840d0dc63db5354452481',
    'digoxin-thiazide': '80fcd96f7075103fcbe9dea7d3b15e5eee03d8385226e000904aa52f5e8c65bf',
    'corticosteroid-nsaid': '4955bd26840ab4111266a65e6353bd07e3bfa471cba30b397665ef80fd0cf1a7',
    'fluoroquinolone-cations': 'ad864b9afe2249eb9db7be27d41df37190de7dfee8b42da873be9daf61b1ce52',
    'levothyroxine-cations': '8070ee5e5b0c609c14f0023a88bb1c95770779bfc3ee9331218d38773a85ec23',
    // Allergies
    'allergy-penicillin': '0c14833418a929696236e7a19e0348e011b064c76a7c42845f7efe8ace68d30d',
    'allergy-sulfa': 'd005842fa24a78248203a16a6857b4ae44662b32e3b8b8c6036edbcda7829ea9',
    'allergy-nsaid': '1d4554f9cae92b426189ffa7521ad9cf0c20bf86c3de7d448801652ecb9ca2ac',
    'allergy-cephalosporin': 'ce3a2453d8dd1989441a4b1d3dcdd533635482d01f716704ef3bca689dcb92f6',
    'allergy-opioid': '3af8bdde4c94f828550cb3c8f83a1deca53fa7a029428f408ba4b8549f89ce41',
    // Older adults
    'older-long-acting-sulfonylurea': '6ffa1d0b4ce02b1d35d28174a6a2b94d2a449f861752f2cefcd38d4317ea8353',
    'older-first-gen-antihistamine': '8e542d2fe2bd704c51c44b596ed7f886e6ddefaf4919883bdf67e1e7ae43c7d7',
    'older-benzodiazepine': '355684a8b7ec1c0189b8b836481c5c41641d10be79513fc656182284accc4e99',
    'older-tca': '8c3f18f444d37d424baa93f7d6f58c5223a48ca26d16d6373d97ac3f91d714f2',
    'older-regular-nsaid': 'f0eae33125d876414f6c5856b74bcc7d6f0761ff9f5ba0bb3742c7fff3533f21',
    // Children
    'child-aspirin-under-16': '72a10714c3c8680f061995643a9c2e50aed8eac149ba9298cf09dd1e1abad945',
    'child-codeine-under-12': '401c16abb51da80f13ebc6f9f1fdc3c7d9fce07c258e3c613f4efca9c790c85e',
    'child-tramadol-under-12': 'd2ea77b04ce9a52cc2565b83448979d758d691012588cb499ca865b3a7b34aaa',
    'child-promethazine-under-2': 'af1d37b78111921a738d3ccee63f9eed382a8bc983c7de03f1ef427ab823cbd8',
    'child-cough-cold-promethazine-under-6': 'a2a08a22c17ccdeabcf71fbeb68024901a8fdc80fa47862793137a0a2bde1b61',
    'child-night-nurse-under-16': 'bbcfacb93bd987bb81d2252ca71fce2f9dfce646114371fa1bf44d3a8112f0b1',
    // Monitoring
    'monitor-warfarin-inr': '52c5a6d74efb5edfc20725d3b640e7bd18081ad8afbc0f60d2fe62dc0a355d95',
    'monitor-metformin-kidneys': 'dc3dddb259846cbe713b710c9ccb95c4b8ec4d52a40899aee24a1d26ff9144d9',
    'monitor-metformin-b12': 'd0661613a6eca35cd9a48a6694cfb5cbaee4ee6aa7d7bf48399e4cb24b590aa8',
    'monitor-diabetes-hba1c': '7da46e20c2c41bbd1e86294122ff447a3c5108f82d011f12142b6341ce428aad',
    'monitor-acei-kidneys-potassium': '682edf75bb5e4259817ed9f299c8bfd9f2abfb6a4e56177c3c212a152644bd26',
    'monitor-arb-kidneys-potassium': '64781edfe1f6846892d5f1b7b0c14de94f2675c3b3b1b592ca09cfd2286b9736',
    'monitor-spironolactone-potassium': '956b5e394a5699089c7b18747c6eaa8ac75e80450c85b105e968f76bec09fd2b',
    'monitor-bp-home': 'cadcbf7b075fe854bb4067b83ddcea079b18cd010d867e34798b9e5485cd5e57',
    'monitor-statin-lipids': '68bc282af50528f80d146e25127330c76dd8ca093956bca51048212a5bbe5dfb',
    'monitor-lithium-level': '9edbda2eb0f62bd422516859f7f215bdeaf591a0598f97bcae394735f19b67ba',
    'monitor-methotrexate-bloods': 'fe5ffb1659d78d5ed041d7f6541618bea9fe18288f91a64b4025307c06db023e',
    'monitor-levothyroxine-tsh': '4c2fa19c0d794e7aebf625211c73f065c165d36ac39a0d4ba67c3ddcfaee7390',
    // Lab results
    'lab-metformin-egfr-30': '896d83412f69adc80dd4e8616e9982ff1def6a299cd9e2c41a53088a8675de01',
    'lab-metformin-egfr-45': '5268c77417d436d3ef17a9ea80cee1a2f386de7bdd82a22c9fdbbf665322914c',
    'lab-inr-8': '71375091565433ee6b677776a87a845c2ffdf3d9c5e917e8a931e39db26eee18',
    'lab-inr-5': '0fee965af3ff0cc5bb87fc124bca7077928d4767723beb9327110c33bb7fc0c5',
    'lab-inr-high-nsaid': '4e887f3d395eb91362b6f0666f71bf520c4aac28fd19c6fc245d1cd029a14ee2',
    'lab-inr-4-5': '8c81723c6af441701e2fe9cb5aecae38bcba3f661ba9451e6f4aaf44548b324f',
    'lab-inr-high': '07bc7f6f3a51d2a691b3feb4678c8a3a9b8eff5ea0a6b8eb67c61d53429bf4bd',
    'lab-potassium-very-high': '8396caa14bb11a9b79ad0ec0949cab94d611f1e9f3128837982f0ae97d77f12e',
    'lab-potassium-high': '9fb06aef76721b9a3f3eb12c7e55192a28b8e4a903133af2a146196580e5725d',
    'lab-potassium-raised': '7a9eae0f899a567d3b4cbce224b7129d9e53ba4dd6b16fd25663b601e8083f14',
  },
};

/**
 * Findings written in engine code rather than in the six rule tables (CHK-7). No sign-off has approved any of them
 * on its own: the pack of 13 October 2026 showed the tables and the catalogue, and the engine 2.2.0 dose checks wait
 * for the top-up review. Each is "not yet reviewed" until a sign-off records its content hash in
 * ENGINE_APPROVED_CONTENT (signed-off-mode.ts says how the hash is made). The signed-off-only mode never runs them before that.
 */
export interface EngineCodedRule {
  id: string;
  /** Rule ids on findings that this entry covers: exactly `id`, or every id starting with `prefix`. */
  prefix?: string;
  what: string;
  review: 'not_yet_reviewed' | 'approved';
}

export const ENGINE_CODED_RULES: readonly EngineCodedRule[] = [
  { id: 'dose-max', prefix: 'dose-max-', what: 'A medicine\'s daily amount over an ingredient\'s label maximum (engine 2.2.0): one same-day finding per medicine.',
    review: 'not_yet_reviewed' },
  { id: 'duplicate', prefix: 'duplicate-', what: 'The same ingredient in more than one medicine, with the daily total against the label maximum.', review: 'not_yet_reviewed' },
  { id: 'paracetamol-lower-maximum', what: 'Paracetamol between 3,000 and 4,000 mg a day under 50 kg or at 75 and over: a lower daily maximum to ask about.',
    review: 'not_yet_reviewed' },
  { id: 'allergy-ingredient', what: 'A medicine with an ingredient named in a recorded allergy, where no allergy family covers it.', review: 'not_yet_reviewed' },
  { id: 'kidney-dose-metformin', what: 'Metformin\'s dose status against the label\'s kidney-adjusted maximum (CHK-11, kidneyDoseLimits option).',
    review: 'not_yet_reviewed' },
];

/** What each sign-off approved of the engine-coded rules, by sign-off id, then rule id: content hash. None yet. */
export const ENGINE_APPROVED_CONTENT: Readonly<Record<string, Readonly<Record<string, string>>>> = {};

/** The engine-coded rule a finding's rule id belongs to, if any. */
export function engineCodedRuleFor(ruleId: string): EngineCodedRule | null {
  return ENGINE_CODED_RULES.find((rule) => (rule.prefix ? ruleId.startsWith(rule.prefix) : ruleId === rule.id)) ?? null;
}