diff --git a/HANDOVER.md b/HANDOVER.md index 25c1654..5cde4de 100644 --- a/HANDOVER.md +++ b/HANDOVER.md @@ -47,7 +47,14 @@ This document provides a comprehensive inventory of services, secrets, external ### 3. Custom Domain & DNS Migration - **Current State**: [dream.yuson.au](https://dream.yuson.au) is mapped via DNS to Cloudflare Pages. -- **Succession Risk**: `yuson.au` is a **personal domain** owned by the original author. If the personal domain lapses or becomes unavailable, the custom URL will stop resolving. +- **Succession Risk**: `yuson.au` is a **personal domain** owned by the original author. If the personal + domain lapses or becomes unavailable, the custom URL will stop resolving. +- **BOTH APPLICATIONS SHARE THIS DOMAIN.** SCRATCH, the drug-protocol source of truth, is served at + [scratch.yuson.au](https://scratch.yuson.au) from the same personal domain. If `yuson.au` lapses, + clinicians lose the reference handbook and the app that records the encounter **at the same time**, + and DREAM's `npm run protocols:sync` stops resolving. Treat moving both to an institutional domain + as a single piece of work, not two. The SCRATCH repository is `monchee/drug-library`; its Cloudflare + Pages project is named `scratch`. - **Domain Independence**: In `vite.config.ts`, `base: './'` is configured, ensuring all bundle assets and routes use relative paths. The application is completely domain-agnostic and functions identically under any hostname, subdirectory, or port. - **Fallback URL**: [anaesthetic-allergy-log-7ya.pages.dev](https://anaesthetic-allergy-log-7ya.pages.dev) — **not** `dream.pages.dev`, which does not resolve. A Pages project's `*.pages.dev` hostname is diff --git a/MAINTAINERS.md b/MAINTAINERS.md index 17870d5..a9cb9da 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -2,7 +2,7 @@ ## Overview -DREAM (Drug Reaction Evaluation & Anaesthetic Management) is a private, local-first clinical Progressive Web App designed for the Royal Prince Alfred Hospital (RPAH) Department of Clinical Immunology & Allergy. It guides clinicians through perioperative anaesthetic allergy workups—from REDCap patient record import and tailored testing plan creation to skin prick/intradermal test logging, patient handouts, and eMR-ready clinical reports—all processed locally in the browser without transmitting identifiable patient data. Drug protocol definitions and dilution concentrations are tied to the SCRATCH drug library (repository `monchee/drug-library`, hosted at [scratch.pages.dev](https://scratch.pages.dev)), which serves as the upcoming source of truth for drug protocol data that DREAM consumes as a pinned JSON snapshot. +DREAM (Drug Reaction Evaluation & Anaesthetic Management) is a private, local-first clinical Progressive Web App designed for the Royal Prince Alfred Hospital (RPAH) Department of Clinical Immunology & Allergy. It guides clinicians through perioperative anaesthetic allergy workups—from REDCap patient record import and tailored testing plan creation to skin prick/intradermal test logging, patient handouts, and eMR-ready clinical reports—all processed locally in the browser without transmitting identifiable patient data. Drug protocol definitions and dilution concentrations are tied to the SCRATCH drug library (repository `monchee/drug-library`, hosted at [scratch.yuson.au](https://scratch.yuson.au)), which serves as the upcoming source of truth for drug protocol data that DREAM consumes as a pinned JSON snapshot. --- diff --git a/package.json b/package.json index 595e2f5..fedac02 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,8 @@ "prebuild": "node scripts/generate-changelog.mjs", "build": "vite build", "changelog:sync": "node scripts/generate-changelog.mjs", + "protocols:sync": "node scripts/sync-protocols.mjs", + "protocols:generate": "node scripts/generate-drug-masterlist.mjs", "preview": "vite preview", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "lint:fix": "eslint . --ext ts,tsx --fix", diff --git a/scripts/generate-drug-masterlist.mjs b/scripts/generate-drug-masterlist.mjs new file mode 100644 index 0000000..a655c87 --- /dev/null +++ b/scripts/generate-drug-masterlist.mjs @@ -0,0 +1,229 @@ +#!/usr/bin/env node +// Generates src/shared/data/drugMasterlist.generated.ts from +// src/shared/data/protocols.snapshot.json. +// +// Run manually with `npm run protocols:generate`. + +import { readFileSync, writeFileSync } from 'node:fs'; +import { fileURLToPath, pathToFileURL } from 'node:url'; +import { dirname, join } from 'node:path'; + +const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..'); +const SNAPSHOT_PATH = join(ROOT, 'src', 'shared', 'data', 'protocols.snapshot.json'); +const OUTPUT_PATH = join(ROOT, 'src', 'shared', 'data', 'drugMasterlist.generated.ts'); + +export function composeSptNeatConcentration(spt) { + if (!spt) return ''; + const { dilution, concentration } = spt; + if (dilution && concentration) { + return `${dilution} (${concentration})`; + } + if (dilution) return dilution; + if (concentration) return concentration; + return ''; +} + +export function transformSnapshotToProtocols(snapshot) { + const protocols = []; + + for (const drug of snapshot.drugs || []) { + const drugName = drug.title; + const category = drug.dream?.category || ''; + const sourceSlug = drug.slug; + const lastReviewed = drug.last_reviewed; + + for (const protocol of drug.protocols || []) { + const hasSkin = Boolean( + protocol.spt || + (protocol.idt && protocol.idt.length > 0) || + (protocol.test_type && protocol.test_type !== 'challenge') + ); + const hasChallenge = Boolean( + protocol.challenge && + protocol.challenge.steps && + protocol.challenge.steps.length > 0 + ); + + const underReview = Boolean(protocol.under_review); + const needsPharmacyVerification = protocol.needs_pharmacy_verification === true; + + // Case 1: Both skin and challenge -> Split into 2 records + if (hasSkin && hasChallenge) { + // Skin record + protocols.push({ + id: protocol.id, + drugName, + category, + testType: protocol.test_type || 'skin', + presentation: protocol.presentation || '', + sptNeatConcentration: composeSptNeatConcentration(protocol.spt), + diluent: protocol.diluent || '', + idtSteps: (protocol.idt || []).map((step) => ({ + ratio: step.dilution || '', + concentration: step.concentration || '', + })), + challengeSteps: [], + protocolLabel: protocol.label, + sourceSlug, + underReview, + lastReviewed, + ...(needsPharmacyVerification ? { needsPharmacyVerification: true } : {}), + }); + + // Challenge record + protocols.push({ + id: `${protocol.id}-challenge`, + drugName, + category, + testType: 'challenge', + presentation: protocol.presentation || '', + sptNeatConcentration: '', + diluent: '', + idtSteps: [], + challengeSteps: (protocol.challenge.steps || []).map((step, idx) => ({ + step: idx + 1, + dose: step.dose || '', + volume: step.volume || '', + cumulative: step.cumulative || '', + })), + protocolLabel: `${protocol.label} Challenge`, + sourceSlug, + underReview, + lastReviewed, + ...(needsPharmacyVerification ? { needsPharmacyVerification: true } : {}), + }); + } else if (hasChallenge) { + // Only challenge record + protocols.push({ + id: protocol.id, + drugName, + category, + testType: 'challenge', + presentation: protocol.presentation || '', + sptNeatConcentration: '', + diluent: '', + idtSteps: [], + challengeSteps: (protocol.challenge?.steps || []).map((step, idx) => ({ + step: idx + 1, + dose: step.dose || '', + volume: step.volume || '', + cumulative: step.cumulative || '', + })), + protocolLabel: protocol.label, + sourceSlug, + underReview, + lastReviewed, + ...(needsPharmacyVerification ? { needsPharmacyVerification: true } : {}), + }); + } else { + // Skin only record + protocols.push({ + id: protocol.id, + drugName, + category, + testType: protocol.test_type || 'skin', + presentation: protocol.presentation || '', + sptNeatConcentration: composeSptNeatConcentration(protocol.spt), + diluent: protocol.diluent || '', + idtSteps: (protocol.idt || []).map((step) => ({ + ratio: step.dilution || '', + concentration: step.concentration || '', + })), + challengeSteps: [], + protocolLabel: protocol.label, + sourceSlug, + underReview, + lastReviewed, + ...(needsPharmacyVerification ? { needsPharmacyVerification: true } : {}), + }); + } + } + } + + return protocols; +} + +function formatStringLiteral(str) { + return JSON.stringify(str); +} + +export function generateTypeScript(protocols) { + const lines = [ + '// AUTO-GENERATED from src/shared/data/protocols.snapshot.json.', + '// Do NOT edit this file directly. Run `npm run protocols:generate` to regenerate.', + '', + "import type { DrugProtocol, IDTStep, ChallengeStep } from '@features/testing/types';", + '', + '// Compact helpers for readability', + 'const s = (ratio: string, concentration: string): IDTStep => ({ ratio, concentration });', + 'const c = (step: number, dose: string, volume: string, cumulative: string): ChallengeStep => ({ step, dose, volume, cumulative });', + '', + 'export const GENERATED_PROTOCOLS: DrugProtocol[] = [', + ]; + + for (const p of protocols) { + lines.push(' {'); + lines.push(` id: ${formatStringLiteral(p.id)},`); + lines.push(` drugName: ${formatStringLiteral(p.drugName)},`); + if (p.needsPharmacyVerification) { + lines.push(' needsPharmacyVerification: true,'); + } + lines.push(` category: ${formatStringLiteral(p.category)},`); + lines.push(` testType: ${formatStringLiteral(p.testType)},`); + lines.push(` presentation: ${formatStringLiteral(p.presentation)},`); + lines.push(` sptNeatConcentration: ${formatStringLiteral(p.sptNeatConcentration)},`); + lines.push(` diluent: ${formatStringLiteral(p.diluent)},`); + + if (p.idtSteps.length === 0) { + lines.push(' idtSteps: [],'); + } else { + const idtFormatted = p.idtSteps + .map((step) => `s(${formatStringLiteral(step.ratio)}, ${formatStringLiteral(step.concentration)})`) + .join(', '); + lines.push(` idtSteps: [${idtFormatted}],`); + } + + if (p.challengeSteps.length === 0) { + lines.push(' challengeSteps: [],'); + } else { + const challengeFormatted = p.challengeSteps + .map((step) => `c(${step.step}, ${formatStringLiteral(step.dose)}, ${formatStringLiteral(step.volume)}, ${formatStringLiteral(step.cumulative)})`) + .join(', '); + lines.push(` challengeSteps: [${challengeFormatted}],`); + } + + lines.push(` protocolLabel: ${formatStringLiteral(p.protocolLabel)},`); + if (p.sourceSlug) { + lines.push(` sourceSlug: ${formatStringLiteral(p.sourceSlug)},`); + } + if (p.underReview !== undefined) { + lines.push(` underReview: ${p.underReview},`); + } + if (p.lastReviewed) { + lines.push(` lastReviewed: ${formatStringLiteral(p.lastReviewed)},`); + } + lines.push(' },'); + } + + lines.push('];'); + lines.push(''); + return lines.join('\n'); +} + +export function generateDrugMasterlist(snapshotPath = SNAPSHOT_PATH, outputPath = OUTPUT_PATH) { + const snapshotContent = readFileSync(snapshotPath, 'utf8'); + const snapshot = JSON.parse(snapshotContent); + const protocols = transformSnapshotToProtocols(snapshot); + const code = generateTypeScript(protocols); + writeFileSync(outputPath, code, 'utf8'); + return { protocolCount: protocols.length }; +} + +export function main() { + const { protocolCount } = generateDrugMasterlist(); + console.log(`Generated ${protocolCount} protocol records in src/shared/data/drugMasterlist.generated.ts`); +} + +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + main(); +} diff --git a/scripts/sync-protocols.mjs b/scripts/sync-protocols.mjs new file mode 100644 index 0000000..544c4bf --- /dev/null +++ b/scripts/sync-protocols.mjs @@ -0,0 +1,281 @@ +#!/usr/bin/env node +// Syncs src/shared/data/protocols.snapshot.json from SCRATCH and regenerates +// src/shared/data/drugMasterlist.generated.ts. +// +// Run manually with `npm run protocols:sync`. +// Do NOT put in prebuild or any automatic hook. + +import { existsSync, readFileSync, writeFileSync } from 'node:fs'; +import { fileURLToPath, pathToFileURL } from 'node:url'; +import { dirname, join } from 'node:path'; +import { generateDrugMasterlist } from './generate-drug-masterlist.mjs'; + +const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..'); +const SNAPSHOT_PATH = join(ROOT, 'src', 'shared', 'data', 'protocols.snapshot.json'); +const DEFAULT_LOCAL_PATH = '/Users/monchee/Projects/scratch/docs/api/protocols.json'; + +// SCRATCH's published protocol export. Verified serving the SCRATCH handbook. +// Note: scratch.pages.dev belongs to MIT Scratch, not this project — a Pages +// project only gets its requested *.pages.dev name if it is free, and it was not. +export const PUBLISHED_PROTOCOLS_URL = 'https://scratch.yuson.au/api/protocols.json'; + +const SUPPORTED_SCHEMA_VERSIONS = ['1.0']; + +export function computeDoseLevelDiff(oldSnapshot, newSnapshot) { + const diffs = []; + const oldDrugs = new Map((oldSnapshot?.drugs || []).map((d) => [d.slug, d])); + const newDrugs = new Map((newSnapshot?.drugs || []).map((d) => [d.slug, d])); + + for (const [slug, newDrug] of newDrugs) { + const oldDrug = oldDrugs.get(slug); + if (!oldDrug) { + diffs.push({ + drug: newDrug.title || slug, + type: 'DRUG_ADDED', + details: [`New drug added with ${newDrug.protocols?.length || 0} protocol(s)`], + }); + continue; + } + + const drugChanges = []; + if (oldDrug.version !== newDrug.version) { + drugChanges.push(`version: "${oldDrug.version}" -> "${newDrug.version}"`); + } + if (oldDrug.last_reviewed !== newDrug.last_reviewed) { + drugChanges.push(`last_reviewed: "${oldDrug.last_reviewed}" -> "${newDrug.last_reviewed}"`); + } + + const oldProtocols = new Map((oldDrug.protocols || []).map((p) => [p.id, p])); + const newProtocols = new Map((newDrug.protocols || []).map((p) => [p.id, p])); + + for (const [pId, newP] of newProtocols) { + const oldP = oldProtocols.get(pId); + if (!oldP) { + drugChanges.push(`Protocol [${pId}] added: label="${newP.label}", type="${newP.test_type}"`); + continue; + } + + const pChanges = []; + const scalarFields = ['label', 'test_type', 'presentation', 'diluent', 'under_review', 'needs_pharmacy_verification']; + for (const field of scalarFields) { + if (oldP[field] !== newP[field]) { + pChanges.push(`${field}: ${JSON.stringify(oldP[field])} -> ${JSON.stringify(newP[field])}`); + } + } + + // Compare spt + if (JSON.stringify(oldP.spt) !== JSON.stringify(newP.spt)) { + if (!oldP.spt && newP.spt) { + pChanges.push(`spt: added (${newP.spt.dilution || ''} / ${newP.spt.concentration || ''})`); + } else if (oldP.spt && !newP.spt) { + pChanges.push('spt: removed'); + } else { + for (const k of ['dilution', 'concentration', 'positive_control', 'negative_control']) { + if (oldP.spt?.[k] !== newP.spt?.[k]) { + pChanges.push(`spt.${k}: ${JSON.stringify(oldP.spt?.[k])} -> ${JSON.stringify(newP.spt?.[k])}`); + } + } + } + } + + // Compare idt + if (JSON.stringify(oldP.idt) !== JSON.stringify(newP.idt)) { + const oldIdt = oldP.idt || []; + const newIdt = newP.idt || []; + if (oldIdt.length !== newIdt.length) { + pChanges.push(`idt.length: ${oldIdt.length} -> ${newIdt.length}`); + } + const maxLen = Math.max(oldIdt.length, newIdt.length); + for (let i = 0; i < maxLen; i++) { + const sOld = oldIdt[i]; + const sNew = newIdt[i]; + if (!sOld && sNew) { + pChanges.push(`idt[${i}]: added (ratio: ${sNew.dilution}, conc: ${sNew.concentration})`); + } else if (sOld && !sNew) { + pChanges.push(`idt[${i}]: removed`); + } else if (sOld && sNew) { + if (sOld.dilution !== sNew.dilution) { + pChanges.push(`idt[${i}].dilution: "${sOld.dilution}" -> "${sNew.dilution}"`); + } + if (sOld.concentration !== sNew.concentration) { + pChanges.push(`idt[${i}].concentration: "${sOld.concentration}" -> "${sNew.concentration}"`); + } + if (sOld.preparation !== sNew.preparation) { + pChanges.push(`idt[${i}].preparation: "${sOld.preparation}" -> "${sNew.preparation}"`); + } + } + } + } + + // Compare challenge + if (JSON.stringify(oldP.challenge) !== JSON.stringify(newP.challenge)) { + const oldSteps = oldP.challenge?.steps || []; + const newSteps = newP.challenge?.steps || []; + if (oldP.challenge?.interval !== newP.challenge?.interval) { + pChanges.push(`challenge.interval: "${oldP.challenge?.interval}" -> "${newP.challenge?.interval}"`); + } + if (oldSteps.length !== newSteps.length) { + pChanges.push(`challenge.steps.length: ${oldSteps.length} -> ${newSteps.length}`); + } + const maxSteps = Math.max(oldSteps.length, newSteps.length); + for (let i = 0; i < maxSteps; i++) { + const cOld = oldSteps[i]; + const cNew = newSteps[i]; + if (!cOld && cNew) { + pChanges.push(`challenge.steps[${i}]: added (dose: ${cNew.dose})`); + } else if (cOld && !cNew) { + pChanges.push(`challenge.steps[${i}]: removed`); + } else if (cOld && cNew) { + for (const k of ['dose', 'volume', 'cumulative', 'interval']) { + if (cOld[k] !== cNew[k]) { + pChanges.push(`challenge.steps[${i}].${k}: ${JSON.stringify(cOld[k])} -> ${JSON.stringify(cNew[k])}`); + } + } + } + } + } + + if (pChanges.length > 0) { + drugChanges.push(`Protocol [${pId}]:\n ` + pChanges.join('\n ')); + } + } + + for (const [pId] of oldProtocols) { + if (!newProtocols.has(pId)) { + drugChanges.push(`Protocol [${pId}] removed`); + } + } + + if (drugChanges.length > 0) { + diffs.push({ + drug: newDrug.title || slug, + type: 'DRUG_MODIFIED', + details: drugChanges, + }); + } + } + + for (const [slug, oldDrug] of oldDrugs) { + if (!newDrugs.has(slug)) { + diffs.push({ + drug: oldDrug.title || slug, + type: 'DRUG_REMOVED', + details: ['Drug removed from snapshot'], + }); + } + } + + return diffs; +} + +export function printDoseDiff(diffs) { + console.log('\n================================================================================'); + console.log('PROTOCOL SYNC REVIEW GATE — DOSE-LEVEL DIFF'); + console.log('================================================================================'); + + if (diffs.length === 0) { + console.log('No dose changes detected between existing snapshot and incoming data.\n'); + return; + } + + for (const diff of diffs) { + console.log(`\n• Drug: ${diff.drug} (${diff.type})`); + for (const detail of diff.details) { + console.log(` ${detail}`); + } + } + + console.log('\n--------------------------------------------------------------------------------'); + console.log(`Summary: ${diffs.length} drug(s) with differences detected.`); + console.log('================================================================================\n'); +} + +export async function fetchSnapshot(sourceUrl) { + const response = await fetch(sourceUrl); + if (!response.ok) { + throw new Error(`Failed to fetch protocols from ${sourceUrl}: ${response.status} ${response.statusText}`); + } + return response.json(); +} + +export function parseArgs(argv) { + let fromPath = null; + let fromUrl = null; + + for (let i = 0; i < argv.length; i++) { + const arg = argv[i]; + if (arg === '--from-path') { + fromPath = argv[i + 1] || DEFAULT_LOCAL_PATH; + i++; + } else if (arg.startsWith('--from-path=')) { + fromPath = arg.slice('--from-path='.length); + } else if (arg === '--from-url') { + fromUrl = argv[i + 1] || PUBLISHED_PROTOCOLS_URL; + i++; + } else if (arg.startsWith('--from-url=')) { + fromUrl = arg.slice('--from-url='.length); + } + } + + return { fromPath, fromUrl }; +} + +export async function syncProtocols(options = {}) { + const { fromPath, fromUrl } = options; + let newSnapshotRaw; + let sourceDescription; + + if (fromUrl) { + sourceDescription = `URL: ${fromUrl}`; + newSnapshotRaw = await fetchSnapshot(fromUrl); + } else { + const targetPath = fromPath || DEFAULT_LOCAL_PATH; + sourceDescription = `Path: ${targetPath}`; + if (!existsSync(targetPath)) { + throw new Error(`Snapshot source path does not exist: ${targetPath}`); + } + const fileContent = readFileSync(targetPath, 'utf8'); + newSnapshotRaw = JSON.parse(fileContent); + } + + if (!SUPPORTED_SCHEMA_VERSIONS.includes(newSnapshotRaw.schema_version)) { + console.error(`\nError: Unrecognised schema_version "${newSnapshotRaw.schema_version}".`); + console.error(`Supported versions: ${SUPPORTED_SCHEMA_VERSIONS.join(', ')}`); + process.exit(1); + } + + let oldSnapshot = null; + if (existsSync(SNAPSHOT_PATH)) { + try { + oldSnapshot = JSON.parse(readFileSync(SNAPSHOT_PATH, 'utf8')); + } catch { + oldSnapshot = null; + } + } + + const diffs = computeDoseLevelDiff(oldSnapshot, newSnapshotRaw); + printDoseDiff(diffs); + + // Write new snapshot + const formattedJson = JSON.stringify(newSnapshotRaw, null, 2) + '\n'; + writeFileSync(SNAPSHOT_PATH, formattedJson, 'utf8'); + console.log(`Updated ${SNAPSHOT_PATH} from ${sourceDescription}`); + + // Regenerate drugMasterlist.generated.ts + const { protocolCount } = generateDrugMasterlist(SNAPSHOT_PATH); + console.log(`Regenerated drugMasterlist.generated.ts with ${protocolCount} protocol records.`); +} + +export async function main() { + const args = parseArgs(process.argv.slice(2)); + try { + await syncProtocols(args); + } catch (err) { + console.error(`Sync failed: ${err.message}`); + process.exit(1); + } +} + +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + main(); +} diff --git a/scripts/verify-order.mjs b/scripts/verify-order.mjs new file mode 100644 index 0000000..3fe9207 --- /dev/null +++ b/scripts/verify-order.mjs @@ -0,0 +1,108 @@ +#!/usr/bin/env node +import { execSync } from 'node:child_process'; +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'node:path'; + +const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..'); + +// 1. Read old drugMasterlist.ts from git HEAD +// Baseline must be a revision where drugMasterlist.ts still holds the hand-typed +// records. HEAD stops being that as soon as this change is committed, so default to +// origin/main and allow an override: `node scripts/verify-order.mjs `. +const BASELINE_REF = process.argv[2] || 'origin/main'; +const oldFileContent = execSync( + `GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_NOSYSTEM=1 git show ${BASELINE_REF}:src/shared/data/drugMasterlist.ts`, + { encoding: 'utf8' } +); + +function extractTuples(fileContent) { + const tuples = []; + const recordRegex = /\{\s*(?:id:\s*['"][^'"]*['"],\s*)?drugName:\s*['"]([^'"]+)['"][\s\S]*?testType:\s*['"]([^'"]+)['"][\s\S]*?protocolLabel:\s*['"]([^'"]*)['"]/g; + let m; + while ((m = recordRegex.exec(fileContent)) !== null) { + tuples.push({ + drugName: m[1], + testType: m[2], + protocolLabel: m[3], + }); + } + return tuples; +} + +const oldTuples = extractTuples(oldFileContent); + +// A baseline that yields almost no records means the ref is not the pre-change file +// (e.g. it already points at the thin module). Fail loudly as a broken check rather +// than silently reporting every position as a mismatch. +const MIN_EXPECTED_BASELINE = 50; +if (oldTuples.length < MIN_EXPECTED_BASELINE) { + console.error( + `BROKEN CHECK: baseline ref '${BASELINE_REF}' yielded only ${oldTuples.length} records ` + + `(expected >= ${MIN_EXPECTED_BASELINE}).\nThis is not a reordering failure — the baseline is wrong. ` + + `Pass a ref where drugMasterlist.ts still holds the hand-typed records.` + ); + process.exit(2); +} + +// 2. Read generated and dream-only files to reconstruct the merged DRUG_MASTERLIST order +const generatedContent = readFileSync(join(ROOT, 'src', 'shared', 'data', 'drugMasterlist.generated.ts'), 'utf8'); +const dreamOnlyContent = readFileSync(join(ROOT, 'src', 'shared', 'data', 'dreamOnlyProtocols.ts'), 'utf8'); + +const generatedTuples = extractTuples(generatedContent); +const dreamOnlyTuples = extractTuples(dreamOnlyContent); + +function findGenTuple(drugName, testType, label) { + const match = generatedTuples.find( + (t) => t.drugName === drugName && t.testType === testType && (!label || t.protocolLabel === label) + ); + if (!match) throw new Error(`Missing generated tuple: ${drugName} (${testType})`); + return match; +} + +// Reconstruct merged array order exactly as drugMasterlist.ts does +const newTuples = [ + findGenTuple('Cis-atracurium', 'skin'), + findGenTuple('Rocuronium', 'skin'), + findGenTuple('Pancuronium', 'skin'), + findGenTuple('Vecuronium', 'skin'), + findGenTuple('Suxamethonium', 'skin'), + ...dreamOnlyTuples.slice(0, 17), + findGenTuple('Cefazolin', 'skin'), + ...dreamOnlyTuples.slice(17, 100), + findGenTuple('Cefazolin', 'challenge'), + ...dreamOnlyTuples.slice(100), +]; + +console.log('================================================================================'); +console.log('DRUG MASTERLIST POSITIONAL ORDER VERIFICATION'); +console.log('================================================================================'); +console.log(`OLD (${BASELINE_REF}) record count: ${oldTuples.length}`); +console.log(`NEW (merged) record count: ${newTuples.length}`); + +let mismatches = 0; +const total = Math.max(oldTuples.length, newTuples.length); + +for (let i = 0; i < total; i++) { + const o = oldTuples[i]; + const n = newTuples[i]; + + if (!o || !n || o.drugName !== n.drugName || o.testType !== n.testType || o.protocolLabel !== n.protocolLabel) { + mismatches++; + console.error(`MISMATCH at index ${i}:`); + console.error(` OLD: ${JSON.stringify(o)}`); + console.error(` NEW: ${JSON.stringify(n)}`); + } +} + +console.log('--------------------------------------------------------------------------------'); +console.log(`Positional mismatches: ${mismatches} out of ${total} positions.`); +if (mismatches === 0) { + console.log('STATUS: PASS (0 mismatches). Clinical saved plan positions are 100% preserved.'); + console.log('================================================================================\n'); + process.exit(0); +} else { + console.error('STATUS: FAIL. Positional ordering was altered!'); + console.log('================================================================================\n'); + process.exit(1); +} diff --git a/src/features/testing/types.ts b/src/features/testing/types.ts index 371588a..b84a9a5 100644 --- a/src/features/testing/types.ts +++ b/src/features/testing/types.ts @@ -11,6 +11,7 @@ export interface ChallengeStep { } export interface DrugProtocol { + id: string; drugName: string; needsPharmacyVerification?: true; category: string; @@ -21,6 +22,9 @@ export interface DrugProtocol { idtSteps: IDTStep[]; challengeSteps: ChallengeStep[]; protocolLabel: string; + sourceSlug?: string; + underReview?: boolean; + lastReviewed?: string; } export interface CustomDrugEntry { @@ -36,6 +40,7 @@ export interface DrugTestRow { drugName: string; customName?: string; protocolIndex?: number; + protocolId?: string; sptWheal: string; idtResults: string[]; notes?: string; diff --git a/src/shared/data/dreamOnlyProtocols.ts b/src/shared/data/dreamOnlyProtocols.ts new file mode 100644 index 0000000..a42db0a --- /dev/null +++ b/src/shared/data/dreamOnlyProtocols.ts @@ -0,0 +1,903 @@ +import type { DrugProtocol, IDTStep, ChallengeStep } from "@features/testing/types"; + +// Compact helpers for readability +const s = (ratio: string, concentration: string): IDTStep => ({ ratio, concentration }); +const c = (step: number, dose: string, volume: string, cumulative: string): ChallengeStep => ({ step, dose, volume, cumulative }); + +/** + * Hand-maintained DREAM protocol records for drugs not yet authored in SCRATCH. + * As drugs are migrated to SCRATCH, their records move to protocols.snapshot.json + * and are removed from this list. + */ +export const DREAM_ONLY_PROTOCOLS: DrugProtocol[] = [ + // ── REVERSAL AGENTS ──────────────────────────────────────────────────────── + { + id: "alone", + drugName: "Sugammadex (Alone)", category: "Reversal Agents", testType: "skin", + presentation: "200mg/2mL", sptNeatConcentration: "Neat (100mg/mL)", diluent: "0.9% sodium chloride", + idtSteps: [s("1:1,000", "0.1mg/mL"), s("1:100", "1mg/mL")], + challengeSteps: [], + protocolLabel: "Alone", + }, + { + id: "rocuronium", + drugName: "Sugammadex (+ Rocuronium)", category: "Reversal Agents", testType: "skin", + presentation: "Mix with Rocuronium 1:1", sptNeatConcentration: "Neat (100mg/10mg/mL)", diluent: "0.9% sodium chloride", + idtSteps: [s("1:1,000", "0.1mg/0.01mg/mL"), s("1:100", "1mg/0.1mg/mL")], + challengeSteps: [], + protocolLabel: "+ Rocuronium", + }, + // ── PENICILLINS ──────────────────────────────────────────────────────── + { + id: "ppl", + drugName: "Penicillin Major", category: "Penicillins", testType: "skin", + presentation: "PPL", sptNeatConcentration: "Neat (0.04mg/20.00mg/mL)", diluent: "Phosphate-buffered saline (1 mL supplied diluent — not plain saline)", + idtSteps: [s("1:10", "0.004mg/2mg"), s("Neat", "0.04mg/20.00mg")], + challengeSteps: [], + protocolLabel: "PPL", + }, + { + id: "md", + drugName: "Penicillin Minor", category: "Penicillins", testType: "skin", + presentation: "MD (Sodium Benzylpenilloate/Mannitol)", sptNeatConcentration: "Neat (0.50mg/20.00mg/mL)", diluent: "Phosphate-buffered saline (1 mL supplied diluent — not plain saline)", + idtSteps: [s("1:10", "0.05mg/2.00mg"), s("Neat", "0.50mg/20.00mg")], + challengeSteps: [], + protocolLabel: "MD", + }, + { + id: "neat-spt", + drugName: "Ampicillin", category: "Penicillins", testType: "skin", + presentation: "1g", sptNeatConcentration: "Neat (100mg/mL)", diluent: "0.9% sodium chloride (reconstitute with 5 mL WFI)", + idtSteps: [s("1:100", "1mg/mL"), s("1:10", "10mg/mL"), s("1:50", "2mg/0.2mg/mL")], + challengeSteps: [], + protocolLabel: "Neat SPT", + }, + { + id: "1-5-spt", + drugName: "Ampicillin", category: "Penicillins", testType: "skin", + presentation: "1g", sptNeatConcentration: "1:5 (20mg/mL)", diluent: "0.9% sodium chloride (reconstitute with 5 mL WFI)", + idtSteps: [s("1:10", "10mg/mL"), s("1:5", "20mg/mL"), s("1:100", "0.06mg/mL")], + challengeSteps: [], + protocolLabel: "1:5 SPT", + }, + { + id: "control", + drugName: "Ampicillin", category: "Penicillins", testType: "control", + presentation: "1g", sptNeatConcentration: "1:5 (20mg/mL)", diluent: "0.9% sodium chloride (reconstitute with 5 mL WFI)", + idtSteps: [s("1:10", "10mg/mL"), s("1:5", "20mg/mL"), s("1:100", "0.02mg/mL")], + challengeSteps: [], + protocolLabel: "Control", + }, + { + id: "neat-spt", + drugName: "Amoxycillin", category: "Penicillins", testType: "skin", + presentation: "1g", sptNeatConcentration: "Neat (100mg/mL)", diluent: "0.9% sodium chloride (reconstitute with 5 mL WFI)", + idtSteps: [s("1:100", "1mg/mL"), s("1:10", "10mg/mL")], + challengeSteps: [], + protocolLabel: "Neat SPT", + }, + { + id: "1-5-spt", + drugName: "Amoxycillin", category: "Penicillins", testType: "skin", + presentation: "1g", sptNeatConcentration: "1:5 (20mg/mL)", diluent: "0.9% sodium chloride (reconstitute with 5 mL WFI)", + idtSteps: [s("1:10", "10mg/mL"), s("1:5", "20mg/mL")], + challengeSteps: [], + protocolLabel: "1:5 SPT", + }, + { + id: "1-1000-start", + drugName: "Benzylpenicillin", category: "Penicillins", testType: "skin", + presentation: "600mg", sptNeatConcentration: "Neat (6mg/mL)", diluent: "0.9% sodium chloride (reconstitute with 1.6 mL)", + idtSteps: [s("1:100", "0.06mg/mL"), s("1:10", "0.6mg/mL")], + challengeSteps: [], + protocolLabel: "1:1,000 start", + }, + { + id: "1-100-start", + drugName: "Benzylpenicillin", category: "Penicillins", testType: "skin", + presentation: "600mg", sptNeatConcentration: "Neat (6mg/mL)", diluent: "0.9% sodium chloride (reconstitute with 1.6 mL)", + idtSteps: [s("1:100", "0.06mg/mL"), s("1:10", "0.6mg/mL"), s("1:50", "2mg/0.2mg/mL")], + challengeSteps: [], + protocolLabel: "1:100 start", + }, + { + id: "control", + drugName: "Benzylpenicillin", category: "Penicillins", testType: "control", + presentation: "600mg", sptNeatConcentration: "Neat (6mg/mL)", diluent: "0.9% sodium chloride (reconstitute with 1.6 mL)", + idtSteps: [s("1:100", "0.06mg/mL"), s("1:10", "0.6mg/mL")], + challengeSteps: [], + protocolLabel: "Control", + }, + { + id: "1-1000-start", + drugName: "Augmentin", category: "Penicillins", testType: "skin", + presentation: "2000mg/200mg", sptNeatConcentration: "1:5 (20mg/2mg/mL)", diluent: "0.9% sodium chloride (reconstitute with 50 mL WFI)", + idtSteps: [s("1:50", "2mg/0.2mg/mL"), s("1:5", "20mg/2mg/mL"), s("1:100", "0.06mg/mL")], + challengeSteps: [], + protocolLabel: "1:1,000 start", + }, + { + id: "1-100-start", + drugName: "Augmentin", category: "Penicillins", testType: "skin", + presentation: "2000mg/200mg", sptNeatConcentration: "1:5 (20mg/2mg/mL)", diluent: "0.9% sodium chloride (reconstitute with 50 mL WFI)", + idtSteps: [s("1:50", "2mg/0.2mg/mL"), s("1:5", "20mg/2mg/mL"), s("1:100", "0.2mg/mL")], + challengeSteps: [], + protocolLabel: "1:100 start", + }, + { + id: "iv", + drugName: "Cephalexin", category: "Penicillins", testType: "skin", + needsPharmacyVerification: true, + presentation: "2mg/mL", sptNeatConcentration: "Neat (2mg/mL)", diluent: "0.9% sodium chloride", + idtSteps: [s("1:100", "0.02mg/mL"), s("1:10", "0.2mg/mL"), s("Neat", "2mg/mL")], + challengeSteps: [], + protocolLabel: "IV", + }, + { + id: "iv", + drugName: "Tazocin", category: "Penicillins", testType: "skin", + presentation: "4g/500mg", sptNeatConcentration: "1:10 (20/2mg/mL)", diluent: "0.9% sodium chloride (reconstitute with 20 mL)", + idtSteps: [s("1:100", "2/0.2mg/mL"), s("1:10", "20/2mg/mL")], + challengeSteps: [], + protocolLabel: "IV", + }, + { + id: "standard", + drugName: "Methoxybenzylpenicillin", category: "Penicillins", testType: "skin", + needsPharmacyVerification: true, + presentation: "", sptNeatConcentration: "", diluent: "", + idtSteps: [], + challengeSteps: [], + protocolLabel: "", + }, + // ── CEPHALOSPORINS ──────────────────────────────────────────────────────── + { + id: "iv", + drugName: "Cefepime", category: "Cephalosporins", testType: "skin", + presentation: "1g", sptNeatConcentration: "Neat (100mg/mL)", diluent: "0.9% sodium chloride (reconstitute with 10 mL WFI)", + idtSteps: [s("1:100", "1mg/mL"), s("1:10", "10mg/mL")], + challengeSteps: [], + protocolLabel: "IV", + }, + { + id: "iv", + drugName: "Cefotaxime", category: "Cephalosporins", testType: "skin", + presentation: "1g", sptNeatConcentration: "Neat (100mg/mL)", diluent: "0.9% sodium chloride (reconstitute with 10 mL WFI)", + idtSteps: [s("1:100", "1mg/mL"), s("1:10", "10mg/mL")], + challengeSteps: [], + protocolLabel: "IV", + }, + { + id: "iv", + drugName: "Ceftazidime", category: "Cephalosporins", testType: "skin", + presentation: "2g", sptNeatConcentration: "Neat (100mg/mL)", diluent: "0.9% sodium chloride (reconstitute with 20 mL WFI)", + idtSteps: [s("1:100", "1mg/mL"), s("1:10", "10mg/mL")], + challengeSteps: [], + protocolLabel: "IV", + }, + { + id: "iv", + drugName: "Ceftriaxone", category: "Cephalosporins", testType: "skin", + presentation: "1g", sptNeatConcentration: "Neat (100mg/mL)", diluent: "0.9% sodium chloride (reconstitute with 10 mL WFI)", + idtSteps: [s("1:100", "1mg/mL"), s("1:10", "10mg/mL")], + challengeSteps: [], + protocolLabel: "IV", + }, + { + id: "iv", + drugName: "Cefuroxime", category: "Cephalosporins", testType: "skin", + presentation: "750mg", sptNeatConcentration: "Neat (29mg/mL)", diluent: "0.9% sodium chloride (reconstitute with 30 mL)", + idtSteps: [s("1:100", "0.29mg/mL"), s("1:10", "2.9mg/mL")], + challengeSteps: [], + protocolLabel: "IV", + }, + // ── HYPNOTICS ──────────────────────────────────────────────────────── + { + id: "iv", + drugName: "Midazolam", category: "Hypnotics", testType: "skin", + presentation: "1mg/mL", sptNeatConcentration: "Neat (1mg/mL)", diluent: "0.9% sodium chloride", + idtSteps: [s("1:100", "0.01mg/mL")], + challengeSteps: [], + protocolLabel: "IV", + }, + { + id: "iv", + drugName: "Propofol", category: "Hypnotics", testType: "skin", + presentation: "10mg/mL", sptNeatConcentration: "Neat (10mg/mL)", diluent: "0.9% sodium chloride", + idtSteps: [s("1:1,000", "0.01mg/mL"), s("1:10", "1mg/mL")], + challengeSteps: [], + protocolLabel: "IV", + }, + { + id: "1-1000-start", + drugName: "Ketamine", category: "Hypnotics", testType: "skin", + presentation: "100mg/mL", sptNeatConcentration: "Neat (100mg/mL)", diluent: "0.9% sodium chloride", + idtSteps: [s("1:1,000", "0.1mg/mL"), s("1:100", "1mg/mL"), s("1:10", "10mg/mL")], + challengeSteps: [], + protocolLabel: "1:1,000 start", + }, + { + id: "1-100-start", + drugName: "Ketamine", category: "Hypnotics", testType: "skin", + presentation: "100mg/mL", sptNeatConcentration: "Neat (100mg/mL)", diluent: "0.9% sodium chloride", + idtSteps: [s("1:1,000", "0.1mg/mL")], + challengeSteps: [], + protocolLabel: "1:100 start", + }, + { + id: "1-1000-start", + drugName: "Thiopental", category: "Hypnotics", testType: "skin", + presentation: "25mg/mL", sptNeatConcentration: "Neat (25mg/mL)", diluent: "0.9% sodium chloride (reconstitute with 20 mL WFI or saline)", + idtSteps: [s("1:1,000", "0.025mg/mL"), s("1:100", "0.25mg/mL"), s("1:10", "2.5mg/mL")], + challengeSteps: [], + protocolLabel: "1:1,000 start", + }, + { + id: "1-100-start", + drugName: "Thiopental", category: "Hypnotics", testType: "skin", + presentation: "25mg/mL", sptNeatConcentration: "Neat (25mg/mL)", diluent: "0.9% sodium chloride (reconstitute with 20 mL WFI or saline)", + idtSteps: [s("1:1,000", "0.025mg/mL"), s("1:100", "0.25mg/mL"), s("1:10", "2.5mg/mL")], + challengeSteps: [], + protocolLabel: "1:100 start", + }, + // ── LOCAL ANAESTHETICS ──────────────────────────────────────────────────────── + { + id: "iv", + drugName: "Lignocaine", category: "Local Anaesthetics", testType: "skin", + presentation: "50mg/5mL", sptNeatConcentration: "Neat (10mg/mL)", diluent: "0.9% sodium chloride", + idtSteps: [s("1:1,000", "0.01mg/mL"), s("1:100", "0.1mg/mL"), s("1:10", "1mg/mL"), s("Neat", "10mg/mL")], + challengeSteps: [], + protocolLabel: "IV", + }, + { + id: "epidural", + drugName: "Mepivacaine", category: "Local Anaesthetics", testType: "skin", + presentation: "66mg/2.2mL", sptNeatConcentration: "Neat (10mg/mL)", diluent: "0.9% sodium chloride (dilute 3% stock with 4.4 mL WFI)", + idtSteps: [s("1:1,000", "0.01mg/mL"), s("1:100", "0.1mg/mL"), s("1:10", "1mg/mL"), s("Neat", "10mg/mL")], + challengeSteps: [], + protocolLabel: "Epidural", + }, + { + id: "epidural", + drugName: "Bupivacaine", category: "Local Anaesthetics", testType: "skin", + presentation: "50mg/20mL (Epidural)", sptNeatConcentration: "Neat (2.5mg/mL)", diluent: "0.9% sodium chloride", + idtSteps: [s("1:1,000", "0.0025mg/mL"), s("1:100", "0.025mg/mL"), s("1:10", "0.25mg/mL"), s("Neat", "2.5mg/mL")], + challengeSteps: [], + protocolLabel: "Epidural", + }, + { + id: "epidural-1", + drugName: "Ropivacaine", category: "Local Anaesthetics", testType: "skin", + presentation: "40mg/20mL", sptNeatConcentration: "Neat (2mg/mL)", diluent: "0.9% sodium chloride", + idtSteps: [s("1:1,000", "0.002mg/mL"), s("1:100", "0.02mg/mL"), s("1:10", "0.2mg/mL"), s("Neat", "2mg/mL")], + challengeSteps: [], + protocolLabel: "Epidural Protocol 1", + }, + { + id: "epidural-2", + drugName: "Ropivacaine", category: "Local Anaesthetics", testType: "skin", + presentation: "40mg/20mL", sptNeatConcentration: "Neat (2mg/mL)", diluent: "0.9% sodium chloride", + idtSteps: [s("1:10", "0.2mg/mL"), s("Neat", "2mg/mL")], + challengeSteps: [], + protocolLabel: "Epidural Protocol 2", + }, + // ── OPIOIDS ──────────────────────────────────────────────────────── + { + id: "iv", + drugName: "Alfentanil", category: "Opioids", testType: "skin", + presentation: "1mg/2mL", sptNeatConcentration: "Neat (0.5mg/mL)", diluent: "0.9% sodium chloride", + idtSteps: [s("1:100", "0.005mg/mL"), s("1:10", "0.05mg/mL")], + challengeSteps: [], + protocolLabel: "IV", + }, + { + id: "iv", + drugName: "Fentanyl", category: "Opioids", testType: "skin", + presentation: "100mcg/2mL", sptNeatConcentration: "Neat (0.05mg/mL)", diluent: "0.9% sodium chloride", + idtSteps: [s("1:100", "0.0005mg/mL"), s("1:10", "0.005mg/mL")], + challengeSteps: [], + protocolLabel: "IV", + }, + { + id: "1-1000-start", + drugName: "Morphine", category: "Opioids", testType: "skin", + presentation: "10mg/mL", sptNeatConcentration: "1:10 (1mg/mL)", diluent: "0.9% sodium chloride", + idtSteps: [s("1:100,000", "0.0001mg/mL"), s("1:100", "0.0005mg/mL")], + challengeSteps: [], + protocolLabel: "1:1,000 start", + }, + { + id: "1-100-start", + drugName: "Morphine", category: "Opioids", testType: "skin", + presentation: "10mg/mL", sptNeatConcentration: "1:10 (1mg/mL)", diluent: "0.9% sodium chloride", + idtSteps: [s("1:100,000", "0.0001mg/mL"), s("1:1,000", "0.01mg/mL")], + challengeSteps: [], + protocolLabel: "1:100 start", + }, + { + id: "1-1000-start", + drugName: "Remifentanil", category: "Opioids", testType: "skin", + presentation: "1mg", sptNeatConcentration: "Neat (0.05mg/mL)", diluent: "0.9% sodium chloride (reconstitute with 20 mL WFI)", + idtSteps: [s("1:100", "0.0005mg/mL"), s("1:10", "0.005mg/mL")], + challengeSteps: [], + protocolLabel: "1:1,000 start", + }, + { + id: "1-100-start", + drugName: "Remifentanil", category: "Opioids", testType: "skin", + presentation: "1mg", sptNeatConcentration: "Neat (0.05mg/mL)", diluent: "0.9% sodium chloride (reconstitute with 20 mL WFI)", + idtSteps: [s("1:100", "0.0005mg/mL"), s("1:10", "0.005mg/mL"), s("1:1,000", "0.01mg/mL")], + challengeSteps: [], + protocolLabel: "1:100 start", + }, + { + id: "iv", + drugName: "Oxycodone", category: "Opioids", testType: "skin", + presentation: "10mg/mL", sptNeatConcentration: "Neat (10mg/mL)", diluent: "0.9% sodium chloride", + idtSteps: [s("1:1,000", "0.01mg/mL"), s("1:100", "0.1mg/mL")], + challengeSteps: [], + protocolLabel: "IV", + }, + // ── ANTISEPTICS ──────────────────────────────────────────────────────── + { + id: "0.02-percent", + drugName: "Chlorhexidine", category: "Antiseptics", testType: "skin", + presentation: "0.02%", sptNeatConcentration: "Neat (0.2mg/mL)", diluent: "0.9% sodium chloride", + idtSteps: [s("1:100", "0.002mg/mL")], + challengeSteps: [], + protocolLabel: "0.02%", + }, + { + id: "1-1000-start", + drugName: "Povidone Iodine", category: "Antiseptics", testType: "skin", + presentation: "10% w/v", sptNeatConcentration: "Neat (100mg/mL)", diluent: "0.9% sodium chloride", + idtSteps: [s("1:10,000", "0.01mg/mL"), s("1:1,000", "0.1mg/mL"), s("1:100", "1mg/mL")], + challengeSteps: [], + protocolLabel: "1:1,000 start", + }, + { + id: "1-100-start", + drugName: "Povidone Iodine", category: "Antiseptics", testType: "skin", + presentation: "10% w/v", sptNeatConcentration: "Neat (100mg/mL)", diluent: "0.9% sodium chloride", + idtSteps: [s("1:10,000", "0.01mg/mL"), s("1:1,000", "0.1mg/mL"), s("1:100", "1mg/mL")], + challengeSteps: [], + protocolLabel: "1:100 start", + }, + // ── PROTON PUMP INHIBITORS ──────────────────────────────────────────────────────── + { + id: "standard", + drugName: "Esomeprazole", category: "Proton Pump Inhibitors", testType: "skin", + presentation: "20mg", sptNeatConcentration: "Neat (20mg/mL)", diluent: "0.9% sodium chloride (dissolve in 1 mL)", + idtSteps: [], + challengeSteps: [], + protocolLabel: "", + }, + { + id: "standard", + drugName: "Lansoprazole", category: "Proton Pump Inhibitors", testType: "skin", + presentation: "30mg", sptNeatConcentration: "Neat (30mg/mL)", diluent: "0.9% sodium chloride (dissolve in 1 mL)", + idtSteps: [], + challengeSteps: [], + protocolLabel: "", + }, + { + id: "standard", + drugName: "Omeprazole", category: "Proton Pump Inhibitors", testType: "skin", + presentation: "20mg", sptNeatConcentration: "Neat (20mg/mL)", diluent: "0.9% sodium chloride (dissolve in 1 mL)", + idtSteps: [], + challengeSteps: [], + protocolLabel: "", + }, + { + id: "iv", + drugName: "Pantoprazole", category: "Proton Pump Inhibitors", testType: "skin", + presentation: "40mg", sptNeatConcentration: "Neat (40mg/mL)", diluent: "0.9% sodium chloride (reconstitute with 10 mL)", + idtSteps: [s("1:1,000", "0.04mg/mL"), s("1:100", "0.4mg/mL"), s("1:10", "4mg/mL")], + challengeSteps: [], + protocolLabel: "IV", + }, + { + id: "standard", + drugName: "Rabeprazole", category: "Proton Pump Inhibitors", testType: "skin", + presentation: "40mg", sptNeatConcentration: "Neat (40mg/mL)", diluent: "0.9% sodium chloride (dissolve in 1 mL)", + idtSteps: [], + challengeSteps: [], + protocolLabel: "", + }, + // ── OTHERS ──────────────────────────────────────────────────────── + { + id: "sc", + drugName: "Actrapid (Insulin)", category: "Others", testType: "skin", + presentation: "100units/mL", sptNeatConcentration: "Neat (100 U/mL)", diluent: "0.9% sodium chloride", + idtSteps: [s("1:20", "5U/mL")], + challengeSteps: [], + protocolLabel: "S/C", + }, + { + id: "iv", + drugName: "Azithromycin", category: "Others", testType: "skin", + presentation: "500mg", sptNeatConcentration: "Neat (100mg/mL)", diluent: "0.9% sodium chloride (reconstitute with 4.8 mL WFI)", + idtSteps: [s("1:10,000", "0.01mg/mL"), s("1:1,000", "0.1mg/mL"), s("1:10", "15mg/mL")], + challengeSteps: [], + protocolLabel: "IV", + }, + { + id: "iv", + drugName: "Betamethasone", category: "Others", testType: "experimental", + presentation: "5.7mg/mL", sptNeatConcentration: "Neat (5.7mg/mL)", diluent: "0.9% sodium chloride", + idtSteps: [s("1:100", "0.057mg/mL"), s("1:15", "0.38mg/mL")], + challengeSteps: [], + protocolLabel: "IV", + }, + { + id: "suspension", + drugName: "Cefuroxime Suspension", category: "Others", testType: "skin", + needsPharmacyVerification: true, + presentation: "125mg/5mL", sptNeatConcentration: "Neat (25mg/mL)", diluent: "", + idtSteps: [s("1:100", "0.29mg/mL")], + challengeSteps: [], + protocolLabel: "Suspension", + }, + { + id: "iv", + drugName: "Ciprofloxacin", category: "Others", testType: "skin", + presentation: "200mg/100mL", sptNeatConcentration: "Neat (2mg/mL)", diluent: "0.9% sodium chloride", + idtSteps: [s("1:100", "0.04mg/mL")], + challengeSteps: [], + protocolLabel: "IV", + }, + { + id: "iv", + drugName: "Clindamycin", category: "Others", testType: "skin", + presentation: "300mg/2mL", sptNeatConcentration: "Neat (150mg/mL)", diluent: "0.9% sodium chloride", + idtSteps: [s("1:10", "15mg/mL"), s("1:1,000", "0.005mg/mL")], + challengeSteps: [], + protocolLabel: "IV", + }, + { + id: "sc", + drugName: "Dalteparin", category: "Others", testType: "skin", + presentation: "10000U/mL", sptNeatConcentration: "Neat (10000U/mL)", diluent: "0.9% sodium chloride", + idtSteps: [s("1:1,000", "10U/mL"), s("1:100", "100U/mL"), s("1:10", "1000U/mL")], + challengeSteps: [], + protocolLabel: "SC", + }, + { + id: "iv", + drugName: "Dexamethasone", category: "Others", testType: "skin", + presentation: "4mg/mL", sptNeatConcentration: "Neat (4mg/mL)", diluent: "0.9% sodium chloride", + idtSteps: [s("1:100", "0.04mg/mL"), s("1:10", "0.4mg/mL")], + challengeSteps: [], + protocolLabel: "IV", + }, + { + id: "1-1000-start", + drugName: "Doxycycline", category: "Others", testType: "skin", + presentation: "100mg", sptNeatConcentration: "Neat (10mg/mL)", diluent: "0.9% sodium chloride (reconstitute with 10 mL WFI or saline)", + idtSteps: [s("1:10,000", "0.001mg/mL"), s("1:1,000", "0.01mg/mL"), s("1:100", "0.05mg/mL")], + challengeSteps: [], + protocolLabel: "1:1,000 start", + }, + { + id: "1-100-start", + drugName: "Doxycycline", category: "Others", testType: "skin", + presentation: "100mg", sptNeatConcentration: "Neat (10mg/mL)", diluent: "0.9% sodium chloride (reconstitute with 10 mL WFI or saline)", + idtSteps: [s("1:10,000", "0.001mg/mL"), s("1:1,000", "0.01mg/mL"), s("1:100", "0.02mg/mL")], + challengeSteps: [], + protocolLabel: "1:100 start", + }, + { + id: "iv", + drugName: "Droperidol", category: "Others", testType: "skin", + presentation: "10mg/2mL", sptNeatConcentration: "Neat (5mg/mL)", diluent: "0.9% sodium chloride", + idtSteps: [s("1:1,000", "0.005mg/mL"), s("1:100", "0.05mg/mL")], + challengeSteps: [], + protocolLabel: "IV", + }, + { + id: "sc", + drugName: "Enoxaparin", category: "Others", testType: "skin", + presentation: "100mg/mL", sptNeatConcentration: "Neat (100mg/mL)", diluent: "0.9% sodium chloride", + idtSteps: [s("1:1,000", "0.1mg/mL"), s("1:100", "1mg/mL"), s("1:10", "10mg/mL")], + challengeSteps: [], + protocolLabel: "SC", + }, + { + id: "iv", + drugName: "Fluconazole", category: "Others", testType: "skin", + presentation: "100mg/50mL", sptNeatConcentration: "1:10 (0.2mg/mL)", diluent: "0.9% sodium chloride", + idtSteps: [s("1:1,000", "0.002mg/mL"), s("1:100", "0.02mg/mL"), s("1:10", "0.2mg/mL")], + challengeSteps: [], + protocolLabel: "IV", + }, + { + id: "standard", + drugName: "Glycopyrronium", category: "Others", testType: "experimental", + presentation: "200mcg/1mL", sptNeatConcentration: "Neat (0.2mg/mL)", diluent: "0.9% sodium chloride", + idtSteps: [s("1:1,000", "0.0002mg/mL"), s("1:100", "0.057mg/mL")], + challengeSteps: [], + protocolLabel: "", + }, + { + id: "iv", + drugName: "Granisetron", category: "Others", testType: "skin", + presentation: "3mg/3mL", sptNeatConcentration: "Neat (1mg/mL)", diluent: "0.9% sodium chloride", + idtSteps: [s("1:100", "0.01mg/mL"), s("1:1,000", "0.001mg/mL")], + challengeSteps: [], + protocolLabel: "IV", + }, + { + id: "sc", + drugName: "Heparin", category: "Others", testType: "skin", + presentation: "5000U/mL", sptNeatConcentration: "Neat (5000U/mL)", diluent: "0.9% sodium chloride", + idtSteps: [s("1:1,000", "5U/mL"), s("1:100", "50U/mL"), s("1:10", "500U/mL")], + challengeSteps: [], + protocolLabel: "SC", + }, + { + id: "sc", + drugName: "Humulin NPH (Insulin)", category: "Others", testType: "skin", + presentation: "100units/mL", sptNeatConcentration: "Neat (100 U/mL)", diluent: "0.9% sodium chloride", + idtSteps: [s("1:20", "5U/mL")], + challengeSteps: [], + protocolLabel: "S/C", + }, + { + id: "sc", + drugName: "Humulin R (Insulin)", category: "Others", testType: "skin", + presentation: "100units/mL", sptNeatConcentration: "Neat (100 U/mL)", diluent: "0.9% sodium chloride", + idtSteps: [s("1:20", "5U/mL")], + challengeSteps: [], + protocolLabel: "S/C", + }, + { + id: "iv", + drugName: "Hydrocortisone", category: "Others", testType: "experimental", + presentation: "50mg/mL", sptNeatConcentration: "Neat (50mg/mL)", diluent: "0.9% sodium chloride (reconstitute with 2 mL WFI or saline)", + idtSteps: [s("1:100", "0.50mg/mL"), s("1:10", "5mg/mL")], + challengeSteps: [], + protocolLabel: "IV", + }, + { + id: "standard", + drugName: "Latex", category: "Others", testType: "skin", + presentation: "", sptNeatConcentration: "Neat", diluent: "Neat — no diluent", + idtSteps: [], + challengeSteps: [], + protocolLabel: "", + }, + { + id: "tablet", + drugName: "Levofloxacin", category: "Others", testType: "skin", + needsPharmacyVerification: true, + presentation: "500mg", sptNeatConcentration: "Neat (5mg/mL)", diluent: "0.9% sodium chloride (tablet prep — confirm with pharmacist)", + idtSteps: [s("1:100", "0.05mg/mL")], + challengeSteps: [], + protocolLabel: "Tablet", + }, + { + id: "oral", + drugName: "Levonorgestrel", category: "Others", testType: "skin", + needsPharmacyVerification: true, + presentation: "750mcg tablet", sptNeatConcentration: "Neat", diluent: "0.9% sodium chloride", + idtSteps: [s("1:100", "0.05mg/mL")], + challengeSteps: [], + protocolLabel: "Oral", + }, + { + id: "inj", + drugName: "Medroxyprogesterone", category: "Others", testType: "skin", + presentation: "150mg/1mL", sptNeatConcentration: "1:3 (50mg/mL)", diluent: "0.9% sodium chloride", + idtSteps: [s("1:100", "0.05mg/mL"), s("1:10", "5mg/mL")], + challengeSteps: [], + protocolLabel: "Inj", + }, + { + id: "1-1000-start", + drugName: "Metacresol", category: "Others", testType: "skin", + presentation: "1034mg/mL", sptNeatConcentration: "1.5mg/mL", diluent: "0.9% sodium chloride", + idtSteps: [s("1:20", "0.075mg/mL")], + challengeSteps: [], + protocolLabel: "1:1,000 start", + }, + { + id: "1-100-start", + drugName: "Metacresol", category: "Others", testType: "skin", + presentation: "1034mg/mL", sptNeatConcentration: "3mg/mL", diluent: "0.9% sodium chloride", + idtSteps: [s("1:20", "0.15mg/mL")], + challengeSteps: [], + protocolLabel: "1:100 start", + }, + { + id: "iv", + drugName: "Methylprednisolone", category: "Others", testType: "experimental", + presentation: "1g", sptNeatConcentration: "Neat (20mg/mL)", diluent: "0.9% sodium chloride (reconstitute with 5 mL)", + idtSteps: [s("1:100", "0.2mg/mL"), s("1:10", "2mg/mL"), s("1:1,000", "0.04mg/mL")], + challengeSteps: [], + protocolLabel: "IV", + }, + { + id: "iv", + drugName: "Metoclopramide", category: "Others", testType: "skin", + presentation: "10mg/2mL", sptNeatConcentration: "Neat (5mg/mL)", diluent: "0.9% sodium chloride", + idtSteps: [s("1:1,000", "0.005mg/mL"), s("1:100", "0.05mg/mL")], + challengeSteps: [], + protocolLabel: "IV", + }, + { + id: "iv", + drugName: "Metronidazole", category: "Others", testType: "skin", + presentation: "500mg/100mL", sptNeatConcentration: "Neat (5mg/mL)", diluent: "0.9% sodium chloride", + idtSteps: [s("1:100", "0.05mg/mL"), s("1:1,000", "0.025mg/mL")], + challengeSteps: [], + protocolLabel: "IV", + }, + { + id: "inj", + drugName: "Neostigmine", category: "Others", testType: "experimental", + presentation: "2.5mg/1mL", sptNeatConcentration: "Neat (2.5mg/mL)", diluent: "0.9% sodium chloride", + idtSteps: [s("1:1,000", "0.0025mg/mL"), s("1:100", "0.025mg/mL")], + challengeSteps: [], + protocolLabel: "Inj", + }, + { + id: "sc", + drugName: "Novorapid (Insulin)", category: "Others", testType: "skin", + presentation: "100units/mL", sptNeatConcentration: "Neat (100 U/mL)", diluent: "0.9% sodium chloride", + idtSteps: [s("1:20", "5U/mL")], + challengeSteps: [], + protocolLabel: "S/C", + }, + { + id: "iv-contrast", + drugName: "Omnipaque", category: "Others", testType: "skin", + presentation: "350mg I/mL", sptNeatConcentration: "Neat", diluent: "0.9% sodium chloride", + idtSteps: [s("1:100", ""), s("1:10", "")], + challengeSteps: [], + protocolLabel: "IV Contrast", + }, + { + id: "iv", + drugName: "Ondansetron", category: "Others", testType: "skin", + presentation: "4mg/2mL", sptNeatConcentration: "Neat (2mg/mL)", diluent: "0.9% sodium chloride", + idtSteps: [s("1:1,000", "0.002mg/mL"), s("1:100", "0.02mg/mL")], + challengeSteps: [], + protocolLabel: "IV", + }, + { + id: "sc", + drugName: "Optisulin (Insulin)", category: "Others", testType: "skin", + presentation: "100units/mL", sptNeatConcentration: "Neat (100 U/mL)", diluent: "0.9% sodium chloride", + idtSteps: [s("1:20", "5U/mL")], + challengeSteps: [], + protocolLabel: "S/C", + }, + { + id: "iv", + drugName: "Paracetamol", category: "Others", testType: "skin", + presentation: "1000mg/100mL", sptNeatConcentration: "Neat (10mg/mL)", diluent: "0.9% sodium chloride", + idtSteps: [s("1:100", "0.1mg/mL"), s("1:10", "1mg/mL")], + challengeSteps: [], + protocolLabel: "IV", + }, + { + id: "iv", + drugName: "Parecoxib", category: "Others", testType: "skin", + presentation: "40mg", sptNeatConcentration: "Neat (8mg/mL)", diluent: "0.9% sodium chloride (reconstitute with 5 mL)", + idtSteps: [s("1:100", "0.08mg/mL")], + challengeSteps: [], + protocolLabel: "IV", + }, + { + id: "sc", + drugName: "Patent Blue", category: "Others", testType: "skin", + presentation: "50mg/2mL", sptNeatConcentration: "Neat (25mg/mL)", diluent: "0.9% sodium chloride", + idtSteps: [s("1:1,000", "0.025mg/mL"), s("1:100", "0.25mg/mL")], + challengeSteps: [], + protocolLabel: "SC", + }, + { + id: "iv", + drugName: "Protamine", category: "Others", testType: "skin", + presentation: "50mg/5mL", sptNeatConcentration: "Neat (10mg/mL)", diluent: "0.9% sodium chloride", + idtSteps: [s("1:1,000", "0.01mg/mL"), s("0.1:20", "0.05mg/mL")], + challengeSteps: [], + protocolLabel: "IV", + }, + { + id: "sc", + drugName: "Protaphane (Insulin)", category: "Others", testType: "skin", + presentation: "100units/mL", sptNeatConcentration: "Neat (100 U/mL)", diluent: "0.9% sodium chloride", + idtSteps: [s("1:20", "5U/mL")], + challengeSteps: [], + protocolLabel: "S/C", + }, + { + id: "iv", + drugName: "Tranexamic Acid", category: "Others", testType: "skin", + presentation: "500mg/5mL", sptNeatConcentration: "Neat (100mg/mL)", diluent: "0.9% sodium chloride", + idtSteps: [s("1:100", "1mg/mL"), s("1:10", "10mg/mL"), s("1:10,000", "0.01mg/mL")], + challengeSteps: [], + protocolLabel: "IV", + }, + { + id: "iv", + drugName: "Tramadol", category: "Others", testType: "experimental", + presentation: "100mg/2mL", sptNeatConcentration: "Neat (50mg/mL)", diluent: "0.9% sodium chloride", + idtSteps: [s("1:100", "0.5mg/mL")], + challengeSteps: [], + protocolLabel: "IV", + }, + { + id: "inj", + drugName: "Triamcinolone", category: "Others", testType: "experimental", + presentation: "40mg/mL", sptNeatConcentration: "1:10 (4mg/mL)", diluent: "0.9% sodium chloride", + idtSteps: [s("1:1,000", "0.04mg/mL"), s("1:100", "0.4mg/mL"), s("1:10", "4mg/mL")], + challengeSteps: [], + protocolLabel: "Inj", + }, + { + id: "iv-contrast", + drugName: "Ultravist", category: "Others", testType: "skin", + presentation: "46.76g/75mL", sptNeatConcentration: "Neat", diluent: "0.9% sodium chloride", + idtSteps: [s("1:100", ""), s("1:10", "")], + challengeSteps: [], + protocolLabel: "IV Contrast", + }, + { + id: "control", + drugName: "Ultravist", category: "Others", testType: "control", + presentation: "46.76g/75mL", sptNeatConcentration: "Neat", diluent: "0.9% sodium chloride", + idtSteps: [s("1:100", ""), s("1:10", "")], + challengeSteps: [], + protocolLabel: "Control", + }, + { + id: "iv-contrast", + drugName: "Urografin", category: "Others", testType: "skin", + presentation: "", sptNeatConcentration: "Neat", diluent: "0.9% sodium chloride", + idtSteps: [s("1:100", ""), s("1:10", "")], + challengeSteps: [], + protocolLabel: "IV Contrast", + }, + { + id: "iv", + drugName: "Vancomycin", category: "Others", testType: "skin", + presentation: "1g", sptNeatConcentration: "Neat (100mg/mL)", diluent: "0.9% sodium chloride (reconstitute with 5 mL [500 mg] / 10 mL [1 g])", + idtSteps: [s("1:1,000,000", "0.0001mg/mL"), s("1:100", "0.4mg/mL")], + challengeSteps: [], + protocolLabel: "IV", + }, + { + id: "iv-contrast", + drugName: "Visipaque", category: "Others", testType: "skin", + presentation: "320mg I/mL", sptNeatConcentration: "Neat", diluent: "0.9% sodium chloride", + idtSteps: [s("1:100", ""), s("1:10", "")], + challengeSteps: [], + protocolLabel: "IV Contrast", + }, + { + id: "iv", + drugName: "Xylocaine", category: "Others", testType: "skin", + presentation: "50mg/5mL", sptNeatConcentration: "Neat (10mg/mL)", diluent: "0.9% sodium chloride", + idtSteps: [s("1:1,000", "0.01mg/mL"), s("1:100", "0.1mg/mL"), s("1:10", "1mg/mL"), s("Neat", "10mg/mL")], + challengeSteps: [], + protocolLabel: "IV", + }, + { + id: "standard", + drugName: "Methylene Blue", category: "Others", testType: "skin", + needsPharmacyVerification: true, + presentation: "", sptNeatConcentration: "", diluent: "", + idtSteps: [], + challengeSteps: [], + protocolLabel: "", + }, + { + id: "standard", + drugName: "IV Contrast", category: "Others", testType: "skin", + needsPharmacyVerification: true, + presentation: "", sptNeatConcentration: "", diluent: "", + idtSteps: [], + challengeSteps: [], + protocolLabel: "", + }, + { + id: "standard", + drugName: "Atropine", category: "Others", testType: "skin", + needsPharmacyVerification: true, + presentation: "", sptNeatConcentration: "", diluent: "", + idtSteps: [], + challengeSteps: [], + protocolLabel: "", + }, + // ── CHALLENGE / DESENSITISATION ─────────────────────────────────────────── + { + id: "oral-graded-challenge", + drugName: "Amoxycillin Suspension", category: "Others", testType: "challenge", + presentation: "125mg/5mL", sptNeatConcentration: "", diluent: "", + idtSteps: [], + challengeSteps: [c(1, "25mg", "1.0 mL", "25mg"), c(2, "50mg", "2.0 mL", "75mg"), c(3, "100mg", "4.0 mL", "175mg"), c(4, "250mg", "10 mL", "425mg")], + protocolLabel: "Oral Graded Challenge", + }, + { + id: "oral-graded-challenge", + drugName: "Amoxycillin/Clavulanic Acid", category: "Others", testType: "challenge", + presentation: "125mg/31.25mg per 5mL", sptNeatConcentration: "", diluent: "", + idtSteps: [], + challengeSteps: [c(1, "25mg/6.25mg", "1 mL", "25mg/6.25mg"), c(2, "125mg/31.25mg", "5 mL", "150mg/37.5mg"), c(3, "250mg/62.5mg", "10 mL", "400mg/100mg"), c(4, "500mg/125mg", "20 mL", "900mg/225mg")], + protocolLabel: "Oral Graded Challenge", + }, + { + id: "oral-graded-challenge", + drugName: "Cephalexin", category: "Penicillins", testType: "challenge", + presentation: "125mg/5mL suspension", sptNeatConcentration: "", diluent: "", + idtSteps: [], + challengeSteps: [c(1, "25mg", "1.0 mL", "25mg"), c(2, "50mg", "2.0 mL", "75mg"), c(3, "100mg", "4.0 mL", "175mg"), c(4, "250mg", "10 mL", "425mg")], + protocolLabel: "Oral Graded Challenge", + }, + { + id: "oral-graded-challenge", + drugName: "Ciprofloxacin", category: "Others", testType: "challenge", + presentation: "Oral suspension", sptNeatConcentration: "", diluent: "", + idtSteps: [], + challengeSteps: [c(1, "50mg", "1 mL", "50mg"), c(2, "125mg", "2.5 mL", "175mg"), c(3, "250mg", "5 mL", "425mg"), c(4, "500mg", "10 mL", "925mg")], + protocolLabel: "Oral Graded Challenge", + }, + { + id: "oral-graded-challenge", + drugName: "Doxycycline", category: "Others", testType: "challenge", + presentation: "Oral", sptNeatConcentration: "", diluent: "", + idtSteps: [], + challengeSteps: [c(1, "10mg", "", ""), c(2, "25mg", "", ""), c(3, "75mg", "", "")], + protocolLabel: "Oral Graded Challenge", + }, + { + id: "oral-graded-challenge", + drugName: "Flucloxacillin", category: "Penicillins", testType: "challenge", + presentation: "125mg/5mL suspension", sptNeatConcentration: "", diluent: "", + idtSteps: [], + challengeSteps: [c(1, "50mg", "2 mL", "50mg"), c(2, "125mg", "5 mL", "175mg"), c(3, "325mg", "13 mL", "500mg")], + protocolLabel: "Oral Graded Challenge", + }, + { + id: "challenge", + drugName: "Lignocaine", category: "Local Anaesthetics", testType: "challenge", + presentation: "1% 50mg/5mL", sptNeatConcentration: "", diluent: "", + idtSteps: [], + challengeSteps: [c(1, "10mg", "1 mL", "")], + protocolLabel: "Challenge", + }, + { + id: "graded-challenge", + drugName: "Meloxicam", category: "Others", testType: "challenge", + presentation: "Tablet", sptNeatConcentration: "", diluent: "", + idtSteps: [], + challengeSteps: [c(1, "2.5mg", "2.5 mL", "2.5mg"), c(2, "5mg", "5 mL", "7.5mg")], + protocolLabel: "Graded Challenge", + }, + { + id: "oral-graded-challenge", + drugName: "Trimethoprim/Sulfamethoxazole", category: "Others", testType: "challenge", + presentation: "40mg/200mg per 5mL suspension", sptNeatConcentration: "", diluent: "", + idtSteps: [], + challengeSteps: [c(1, "16mg/80mg", "2.0 mL", "16mg/80mg"), c(2, "64mg/320mg", "8.0 mL", "80mg/400mg"), c(3, "80mg/400mg", "10.0 mL", "160mg/800mg")], + protocolLabel: "Oral Graded Challenge", + }, + { + id: "oral-graded-challenge", + drugName: "Trimethoprim", category: "Others", testType: "challenge", + presentation: "Suspension", sptNeatConcentration: "", diluent: "", + idtSteps: [], + challengeSteps: [c(1, "20mg", "2 mL", "20mg"), c(2, "100mg", "10 mL", "120mg"), c(3, "200mg", "20 mL", "320mg")], + protocolLabel: "Oral Graded Challenge", + }, + { + id: "graded-challenge", + drugName: "Voltaren (Diclofenac)", category: "Others", testType: "challenge", + presentation: "Tablet", sptNeatConcentration: "", diluent: "", + idtSteps: [], + challengeSteps: [c(1, "25mg", "1 tablet", "25mg"), c(2, "50mg", "1 tablet", "75mg"), c(3, "100mg", "2 tablets", "175mg")], + protocolLabel: "Graded Challenge", + }, +]; diff --git a/src/shared/data/drugMasterlist.generated.ts b/src/shared/data/drugMasterlist.generated.ts new file mode 100644 index 0000000..3cc7820 --- /dev/null +++ b/src/shared/data/drugMasterlist.generated.ts @@ -0,0 +1,116 @@ +// AUTO-GENERATED from src/shared/data/protocols.snapshot.json. +// Do NOT edit this file directly. Run `npm run protocols:generate` to regenerate. + +import type { DrugProtocol, IDTStep, ChallengeStep } from '@features/testing/types'; + +// Compact helpers for readability +const s = (ratio: string, concentration: string): IDTStep => ({ ratio, concentration }); +const c = (step: number, dose: string, volume: string, cumulative: string): ChallengeStep => ({ step, dose, volume, cumulative }); + +export const GENERATED_PROTOCOLS: DrugProtocol[] = [ + { + id: "iv", + drugName: "Cefazolin", + category: "Cephalosporins", + testType: "skin", + presentation: "1 g powder for injection", + sptNeatConcentration: "Neat (100 mg/mL)", + diluent: "0.9% sodium chloride (reconstitute with 10 mL WFI)", + idtSteps: [s("1:100", "1 mg/mL"), s("1:10", "10 mg/mL")], + challengeSteps: [], + protocolLabel: "IV", + sourceSlug: "cefazolin", + underReview: false, + lastReviewed: "2026-03-28", + }, + { + id: "iv-challenge", + drugName: "Cefazolin", + category: "Cephalosporins", + testType: "challenge", + presentation: "1 g powder for injection", + sptNeatConcentration: "", + diluent: "", + idtSteps: [], + challengeSteps: [c(1, "100 mg", "", ""), c(2, "200 mg", "", ""), c(3, "300 mg", "", ""), c(4, "400 mg", "", "")], + protocolLabel: "IV Challenge", + sourceSlug: "cefazolin", + underReview: false, + lastReviewed: "2026-03-28", + }, + { + id: "iv", + drugName: "Cis-atracurium", + category: "Muscle Relaxants", + testType: "skin", + presentation: "5 mg/2.5 mL (2 mg/mL)", + sptNeatConcentration: "Neat (2 mg/mL)", + diluent: "0.9% sodium chloride", + idtSteps: [s("1:1,000", "0.002 mg/mL")], + challengeSteps: [], + protocolLabel: "IV", + sourceSlug: "cis-atracurium", + underReview: false, + lastReviewed: "2026-08-20", + }, + { + id: "iv", + drugName: "Pancuronium", + category: "Muscle Relaxants", + testType: "skin", + presentation: "4 mg/2 mL (2 mg/mL)", + sptNeatConcentration: "Neat (2 mg/mL)", + diluent: "0.9% sodium chloride", + idtSteps: [s("1:1,000", "0.002 mg/mL"), s("1:100", "0.02 mg/mL")], + challengeSteps: [], + protocolLabel: "IV", + sourceSlug: "pancuronium", + underReview: false, + lastReviewed: "2026-08-20", + }, + { + id: "iv", + drugName: "Rocuronium", + category: "Muscle Relaxants", + testType: "skin", + presentation: "50 mg/5 mL (10 mg/mL)", + sptNeatConcentration: "Neat (10 mg/mL)", + diluent: "0.9% sodium chloride", + idtSteps: [s("1:1,000", "0.01 mg/mL"), s("1:100", "0.1 mg/mL")], + challengeSteps: [], + protocolLabel: "IV", + sourceSlug: "rocuronium", + underReview: false, + lastReviewed: "2026-08-20", + }, + { + id: "iv", + drugName: "Suxamethonium", + category: "Muscle Relaxants", + testType: "skin", + presentation: "100 mg/2 mL (50 mg/mL)", + sptNeatConcentration: "1:5 (10 mg/mL)", + diluent: "0.9% sodium chloride", + idtSteps: [s("1:50,000", "0.001 mg/mL"), s("1:5,000", "0.01 mg/mL"), s("1:500", "0.1 mg/mL")], + challengeSteps: [], + protocolLabel: "IV", + sourceSlug: "suxamethonium", + underReview: false, + lastReviewed: "2026-08-20", + }, + { + id: "iv", + drugName: "Vecuronium", + category: "Muscle Relaxants", + testType: "skin", + presentation: "10 mg powder for injection", + sptNeatConcentration: "Neat (4 mg/mL)", + diluent: "0.9% sodium chloride (reconstitute with 2.5 mL WFI)", + idtSteps: [s("1:1,000", "0.004 mg/mL"), s("1:100", "0.04 mg/mL")], + challengeSteps: [], + protocolLabel: "IV", + sourceSlug: "vecuronium", + underReview: false, + lastReviewed: "2026-08-20", + }, +]; diff --git a/src/shared/data/drugMasterlist.test.ts b/src/shared/data/drugMasterlist.test.ts index b8fc4eb..dfc9f1e 100644 --- a/src/shared/data/drugMasterlist.test.ts +++ b/src/shared/data/drugMasterlist.test.ts @@ -1,17 +1,122 @@ import { describe, expect, it } from 'vitest'; -import { DRUG_MASTERLIST, getSkinProtocolsForDrug } from './drugMasterlist'; +import expectedOrder from './expectedProtocolOrder.json'; +import { + DRUG_MASTERLIST, + DREAM_ONLY_PROTOCOLS, + MASTERLIST_CATEGORIES, + getProtocolsForDrug, + getSkinProtocolsForDrug, + getDrugsByCategory, + getChallengeDrugsByCategory, +} from './drugMasterlist'; + +describe('drugMasterlist structure & integrity', () => { + it('contains exactly 116 protocol records in the merged masterlist', () => { + expect(DRUG_MASTERLIST).toHaveLength(116); + expect(DREAM_ONLY_PROTOCOLS).toHaveLength(109); + }); + + it('defines a non-empty id for every protocol entry', () => { + expect(DRUG_MASTERLIST.every(protocol => typeof protocol.id === 'string' && protocol.id.trim().length > 0)).toBe(true); + }); -describe('drugMasterlist diluents', () => { it('defines a diluent string for every protocol entry', () => { expect(DRUG_MASTERLIST.every(protocol => typeof protocol.diluent === 'string')).toBe(true); }); + it('ensures protocol IDs are unique within each drug name', () => { + const protocolsByDrug = new Map(); + for (const protocol of DRUG_MASTERLIST) { + const ids = protocolsByDrug.get(protocol.drugName) || []; + ids.push(protocol.id); + protocolsByDrug.set(protocol.drugName, ids); + } + + for (const [drugName, ids] of protocolsByDrug.entries()) { + const uniqueIds = new Set(ids); + expect(uniqueIds.size, `Duplicate protocol id detected for drug: ${drugName}`).toBe(ids.length); + } + }); + + it('assigns every protocol to a valid category in MASTERLIST_CATEGORIES', () => { + expect(DRUG_MASTERLIST.every(protocol => MASTERLIST_CATEGORIES.includes(protocol.category))).toBe(true); + }); + + it('preserves standard MASTERLIST_CATEGORIES ordering', () => { + expect(MASTERLIST_CATEGORIES).toEqual([ + 'Muscle Relaxants', + 'Reversal Agents', + 'Penicillins', + 'Cephalosporins', + 'Hypnotics', + 'Local Anaesthetics', + 'Opioids', + 'Antiseptics', + 'Proton Pump Inhibitors', + 'Others', + ]); + }); +}); + +describe('drugMasterlist array ordering & backwards compatibility', () => { + // DREAM selects protocol variants by array index (protocolIndex), so a positional + // shift silently re-points a saved clinical plan at a different dose. The expected + // order is a committed fixture rather than a `git show` of a previous revision: + // that baseline disappears the moment this change merges, and a check that quietly + // stops working is worse than no check. + it('preserves the frozen positional ordering of all 116 records', () => { + expect(expectedOrder.count).toBe(116); + expect(expectedOrder.order).toHaveLength(116); + expect(DRUG_MASTERLIST).toHaveLength(116); + + for (let i = 0; i < expectedOrder.order.length; i++) { + const want = expectedOrder.order[i]; + const got = DRUG_MASTERLIST[i]; + expect(got.drugName, `Position ${i} drugName mismatch`).toBe(want.drugName); + expect(got.testType, `Position ${i} testType mismatch`).toBe(want.testType); + expect(got.protocolLabel, `Position ${i} protocolLabel mismatch`).toBe(want.protocolLabel); + } + }); +}); + +describe('drugMasterlist diluents & snapshot data', () => { it('uses sourced diluent values for representative RTU and reconstitution-volume drugs', () => { expect(getSkinProtocolsForDrug('Rocuronium')[0].diluent).toBe('0.9% sodium chloride'); + expect(getSkinProtocolsForDrug('Cis-atracurium')[0].diluent).toBe('0.9% sodium chloride'); + expect(getSkinProtocolsForDrug('Vecuronium')[0].diluent).toBe('0.9% sodium chloride (reconstitute with 2.5 mL WFI)'); expect(getSkinProtocolsForDrug('Cefazolin')[0].diluent).toBe('0.9% sodium chloride (reconstitute with 10 mL WFI)'); expect(getSkinProtocolsForDrug('Pantoprazole')[0].diluent).toBe('0.9% sodium chloride (reconstitute with 10 mL)'); expect(getSkinProtocolsForDrug('Penicillin Major')[0].diluent).toBe('Phosphate-buffered saline (1 mL supplied diluent — not plain saline)'); }); + + it('correctly populates metadata on generated snapshot records', () => { + const rocuronium = getSkinProtocolsForDrug('Rocuronium')[0]; + expect(rocuronium.sourceSlug).toBe('rocuronium'); + expect(rocuronium.underReview).toBe(false); + expect(rocuronium.lastReviewed).toBe('2026-08-20'); + expect(rocuronium.id).toBe('iv'); + }); + + it('correctly splits Cefazolin into skin and challenge records', () => { + const cefazolinAll = getProtocolsForDrug('Cefazolin'); + expect(cefazolinAll).toHaveLength(2); + + const skin = cefazolinAll.find(p => p.testType === 'skin'); + expect(skin).toBeDefined(); + expect(skin?.id).toBe('iv'); + expect(skin?.protocolLabel).toBe('IV'); + expect(skin?.sptNeatConcentration).toBe('Neat (100 mg/mL)'); + expect(skin?.idtSteps).toHaveLength(2); + expect(skin?.challengeSteps).toHaveLength(0); + + const challenge = cefazolinAll.find(p => p.testType === 'challenge'); + expect(challenge).toBeDefined(); + expect(challenge?.id).toBe('iv-challenge'); + expect(challenge?.protocolLabel).toBe('IV Challenge'); + expect(challenge?.challengeSteps).toHaveLength(4); + expect(challenge?.idtSteps).toHaveLength(0); + expect(challenge?.diluent).toBe(''); + }); }); describe('drugMasterlist pharmacy verification flags', () => { @@ -41,3 +146,33 @@ describe('drugMasterlist pharmacy verification flags', () => { expect(cephalexinChallenge?.needsPharmacyVerification).toBeUndefined(); }); }); + +describe('drugMasterlist helper functions', () => { + it('getProtocolsForDrug returns all protocols for a drug name', () => { + expect(getProtocolsForDrug('Ampicillin')).toHaveLength(3); + expect(getProtocolsForDrug('NonExistentDrug')).toEqual([]); + }); + + it('getSkinProtocolsForDrug excludes challenge-only protocols', () => { + expect(getSkinProtocolsForDrug('Cefazolin')).toHaveLength(1); + expect(getSkinProtocolsForDrug('Amoxycillin Suspension')).toHaveLength(0); + expect(getSkinProtocolsForDrug('Betamethasone')).toHaveLength(1); // experimental + }); + + it('getDrugsByCategory excludes challenge-only drugs and sorts alphabetically', () => { + const byCategory = getDrugsByCategory(); + expect(byCategory['Cephalosporins']).toContain('Cefazolin'); + expect(byCategory['Others']).not.toContain('Amoxycillin Suspension'); + for (const [cat, drugs] of Object.entries(byCategory)) { + const sorted = [...drugs].sort(); + expect(drugs, `Drugs in category ${cat} should be sorted`).toEqual(sorted); + } + }); + + it('getChallengeDrugsByCategory includes challenge drugs grouped by category', () => { + const challengeDrugs = getChallengeDrugsByCategory(); + expect(challengeDrugs['Cephalosporins']).toEqual(['Cefazolin']); + expect(challengeDrugs['Penicillins']).toEqual(['Cephalexin', 'Flucloxacillin']); + expect(challengeDrugs['Others']).toContain('Amoxycillin Suspension'); + }); +}); diff --git a/src/shared/data/drugMasterlist.ts b/src/shared/data/drugMasterlist.ts index 7bea407..7d371f6 100644 --- a/src/shared/data/drugMasterlist.ts +++ b/src/shared/data/drugMasterlist.ts @@ -1,776 +1,56 @@ -import type { DrugProtocol, IDTStep, ChallengeStep } from '@features/testing/types'; - -// Compact helpers for readability -const s = (ratio: string, concentration: string): IDTStep => ({ ratio, concentration }); -const c = (step: number, dose: string, volume: string, cumulative: string): ChallengeStep => ({ step, dose, volume, cumulative }); +import type { DrugProtocol } from '@features/testing/types'; +import { GENERATED_PROTOCOLS } from './drugMasterlist.generated'; +import { DREAM_ONLY_PROTOCOLS } from './dreamOnlyProtocols'; + +export { DREAM_ONLY_PROTOCOLS }; + +function findGenerated( + drugName: string, + testType: 'skin' | 'challenge' | 'control' | 'experimental', + protocolLabel?: string +): DrugProtocol { + const match = GENERATED_PROTOCOLS.find( + (p) => + p.drugName === drugName && + p.testType === testType && + (!protocolLabel || p.protocolLabel === protocolLabel) + ); + if (!match) { + throw new Error(`Missing generated protocol for ${drugName} (${testType}${protocolLabel ? ` - ${protocolLabel}` : ''})`); + } + return match; +} -// Diluent dataset mined from /Users/monchee/Projects/scratch/docs/drugs/*.md. -// Confirm before release: Cephalexin, Levofloxacin, Levonorgestrel, and insulin SPT preparation volumes remain unresolved; Methoxybenzylpenicillin, Cefuroxime Suspension, Methylene Blue, IV Contrast, and Atropine need clinician-filled diluent values. +/** + * Complete drug masterlist for allergy testing and challenges. + * Combines generated protocols from SCRATCH (protocols.snapshot.json) with + * hand-maintained protocols for drugs not yet migrated to SCRATCH. + * + * CRITICAL: The array order is strictly preserved to maintain backwards compatibility + * with saved plans that reference protocolIndex. + */ export const DRUG_MASTERLIST: DrugProtocol[] = [ - // ── MUSCLE RELAXANTS ────────────────────────────────────────────────────── - { - drugName: 'Cis-atracurium', category: 'Muscle Relaxants', testType: 'skin', - presentation: '5mg/2.5mL', sptNeatConcentration: 'Neat (2mg/mL)', diluent: '0.9% sodium chloride', - idtSteps: [s('1:1,000', '0.002mg/mL')], - challengeSteps: [], protocolLabel: 'IV', - }, - { - drugName: 'Rocuronium', category: 'Muscle Relaxants', testType: 'skin', - presentation: '50mg/5mL', sptNeatConcentration: 'Neat (10mg/mL)', diluent: '0.9% sodium chloride', - idtSteps: [s('1:1,000', '0.01mg/mL'), s('1:100', '0.1mg/mL')], - challengeSteps: [], protocolLabel: 'IV', - }, - { - drugName: 'Pancuronium', category: 'Muscle Relaxants', testType: 'skin', - presentation: '4mg/2mL', sptNeatConcentration: 'Neat (2mg/mL)', diluent: '0.9% sodium chloride', - idtSteps: [s('1:1,000', '0.002mg/mL'), s('1:100', '0.02mg/mL')], - challengeSteps: [], protocolLabel: 'IV', - }, - { - drugName: 'Vecuronium', category: 'Muscle Relaxants', testType: 'skin', - presentation: '10mg', sptNeatConcentration: 'Neat (4mg/mL)', diluent: '0.9% sodium chloride (reconstitute with 2.5 mL WFI)', - idtSteps: [s('1:1,000', '0.004mg/mL'), s('1:100', '0.04mg/mL')], - challengeSteps: [], protocolLabel: 'IV', - }, - { - drugName: 'Suxamethonium', category: 'Muscle Relaxants', testType: 'skin', - presentation: '100mg/2mL', sptNeatConcentration: '1:5 (10mg/mL)', diluent: '0.9% sodium chloride', - idtSteps: [s('1:50,000', '0.001mg/mL'), s('1:5,000', '0.01mg/mL'), s('1:500', '0.1mg/mL')], - challengeSteps: [], protocolLabel: 'IV', - }, - - // ── REVERSAL AGENTS ─────────────────────────────────────────────────────── - { - drugName: 'Sugammadex (Alone)', category: 'Reversal Agents', testType: 'skin', - presentation: '200mg/2mL', sptNeatConcentration: 'Neat (100mg/mL)', diluent: '0.9% sodium chloride', - idtSteps: [s('1:1,000', '0.1mg/mL'), s('1:100', '1mg/mL')], - challengeSteps: [], protocolLabel: 'Alone', - }, - { - drugName: 'Sugammadex (+ Rocuronium)', category: 'Reversal Agents', testType: 'skin', - presentation: 'Mix with Rocuronium 1:1', sptNeatConcentration: 'Neat (100mg/10mg/mL)', diluent: '0.9% sodium chloride', - idtSteps: [s('1:1,000', '0.1mg/0.01mg/mL'), s('1:100', '1mg/0.1mg/mL')], - challengeSteps: [], protocolLabel: '+ Rocuronium', - }, - - // ── PENICILLINS ─────────────────────────────────────────────────────────── - { - drugName: 'Penicillin Major', category: 'Penicillins', testType: 'skin', - presentation: 'PPL', sptNeatConcentration: 'Neat (0.04mg/20.00mg/mL)', diluent: 'Phosphate-buffered saline (1 mL supplied diluent — not plain saline)', - idtSteps: [s('1:10', '0.004mg/2mg'), s('Neat', '0.04mg/20.00mg')], - challengeSteps: [], protocolLabel: 'PPL', - }, - { - drugName: 'Penicillin Minor', category: 'Penicillins', testType: 'skin', - presentation: 'MD (Sodium Benzylpenilloate/Mannitol)', sptNeatConcentration: 'Neat (0.50mg/20.00mg/mL)', diluent: 'Phosphate-buffered saline (1 mL supplied diluent — not plain saline)', - idtSteps: [s('1:10', '0.05mg/2.00mg'), s('Neat', '0.50mg/20.00mg')], - challengeSteps: [], protocolLabel: 'MD', - }, - { - drugName: 'Ampicillin', category: 'Penicillins', testType: 'skin', - presentation: '1g', sptNeatConcentration: 'Neat (100mg/mL)', diluent: '0.9% sodium chloride (reconstitute with 5 mL WFI)', - idtSteps: [s('1:100', '1mg/mL'), s('1:10', '10mg/mL'), s('1:50', '2mg/0.2mg/mL')], - challengeSteps: [], protocolLabel: 'Neat SPT', - }, - { - drugName: 'Ampicillin', category: 'Penicillins', testType: 'skin', - presentation: '1g', sptNeatConcentration: '1:5 (20mg/mL)', diluent: '0.9% sodium chloride (reconstitute with 5 mL WFI)', - idtSteps: [s('1:10', '10mg/mL'), s('1:5', '20mg/mL'), s('1:100', '0.06mg/mL')], - challengeSteps: [], protocolLabel: '1:5 SPT', - }, - { - drugName: 'Ampicillin', category: 'Penicillins', testType: 'control', - presentation: '1g', sptNeatConcentration: '1:5 (20mg/mL)', diluent: '0.9% sodium chloride (reconstitute with 5 mL WFI)', - idtSteps: [s('1:10', '10mg/mL'), s('1:5', '20mg/mL'), s('1:100', '0.02mg/mL')], - challengeSteps: [], protocolLabel: 'Control', - }, - { - drugName: 'Amoxycillin', category: 'Penicillins', testType: 'skin', - presentation: '1g', sptNeatConcentration: 'Neat (100mg/mL)', diluent: '0.9% sodium chloride (reconstitute with 5 mL WFI)', - idtSteps: [s('1:100', '1mg/mL'), s('1:10', '10mg/mL')], - challengeSteps: [], protocolLabel: 'Neat SPT', - }, - { - drugName: 'Amoxycillin', category: 'Penicillins', testType: 'skin', - presentation: '1g', sptNeatConcentration: '1:5 (20mg/mL)', diluent: '0.9% sodium chloride (reconstitute with 5 mL WFI)', - idtSteps: [s('1:10', '10mg/mL'), s('1:5', '20mg/mL')], - challengeSteps: [], protocolLabel: '1:5 SPT', - }, - { - drugName: 'Benzylpenicillin', category: 'Penicillins', testType: 'skin', - presentation: '600mg', sptNeatConcentration: 'Neat (6mg/mL)', diluent: '0.9% sodium chloride (reconstitute with 1.6 mL)', - idtSteps: [s('1:100', '0.06mg/mL'), s('1:10', '0.6mg/mL')], - challengeSteps: [], protocolLabel: '1:1,000 start', - }, - { - drugName: 'Benzylpenicillin', category: 'Penicillins', testType: 'skin', - presentation: '600mg', sptNeatConcentration: 'Neat (6mg/mL)', diluent: '0.9% sodium chloride (reconstitute with 1.6 mL)', - idtSteps: [s('1:100', '0.06mg/mL'), s('1:10', '0.6mg/mL'), s('1:50', '2mg/0.2mg/mL')], - challengeSteps: [], protocolLabel: '1:100 start', - }, - { - drugName: 'Benzylpenicillin', category: 'Penicillins', testType: 'control', - presentation: '600mg', sptNeatConcentration: 'Neat (6mg/mL)', diluent: '0.9% sodium chloride (reconstitute with 1.6 mL)', - idtSteps: [s('1:100', '0.06mg/mL'), s('1:10', '0.6mg/mL')], - challengeSteps: [], protocolLabel: 'Control', - }, - { - drugName: 'Augmentin', category: 'Penicillins', testType: 'skin', - presentation: '2000mg/200mg', sptNeatConcentration: '1:5 (20mg/2mg/mL)', diluent: '0.9% sodium chloride (reconstitute with 50 mL WFI)', - idtSteps: [s('1:50', '2mg/0.2mg/mL'), s('1:5', '20mg/2mg/mL'), s('1:100', '0.06mg/mL')], - challengeSteps: [], protocolLabel: '1:1,000 start', - }, - { - drugName: 'Augmentin', category: 'Penicillins', testType: 'skin', - presentation: '2000mg/200mg', sptNeatConcentration: '1:5 (20mg/2mg/mL)', diluent: '0.9% sodium chloride (reconstitute with 50 mL WFI)', - idtSteps: [s('1:50', '2mg/0.2mg/mL'), s('1:5', '20mg/2mg/mL'), s('1:100', '0.2mg/mL')], - challengeSteps: [], protocolLabel: '1:100 start', - }, - { - drugName: 'Cephalexin', category: 'Penicillins', testType: 'skin', - needsPharmacyVerification: true, - presentation: '2mg/mL', sptNeatConcentration: 'Neat (2mg/mL)', diluent: '0.9% sodium chloride', - idtSteps: [s('1:100', '0.02mg/mL'), s('1:10', '0.2mg/mL'), s('Neat', '2mg/mL')], - challengeSteps: [], protocolLabel: 'IV', - }, - { - drugName: 'Tazocin', category: 'Penicillins', testType: 'skin', - presentation: '4g/500mg', sptNeatConcentration: '1:10 (20/2mg/mL)', diluent: '0.9% sodium chloride (reconstitute with 20 mL)', - idtSteps: [s('1:100', '2/0.2mg/mL'), s('1:10', '20/2mg/mL')], - challengeSteps: [], protocolLabel: 'IV', - }, - { - drugName: 'Methoxybenzylpenicillin', category: 'Penicillins', testType: 'skin', - needsPharmacyVerification: true, - presentation: '', sptNeatConcentration: '', diluent: '', - idtSteps: [], - challengeSteps: [], protocolLabel: '', - }, - - // ── CEPHALOSPORINS ──────────────────────────────────────────────────────── - { - drugName: 'Cefazolin', category: 'Cephalosporins', testType: 'skin', - presentation: '1g', sptNeatConcentration: 'Neat (100mg/mL)', diluent: '0.9% sodium chloride (reconstitute with 10 mL WFI)', - idtSteps: [s('1:100', '1mg/mL'), s('1:10', '10mg/mL')], - challengeSteps: [], protocolLabel: 'IV', - }, - { - drugName: 'Cefepime', category: 'Cephalosporins', testType: 'skin', - presentation: '1g', sptNeatConcentration: 'Neat (100mg/mL)', diluent: '0.9% sodium chloride (reconstitute with 10 mL WFI)', - idtSteps: [s('1:100', '1mg/mL'), s('1:10', '10mg/mL')], - challengeSteps: [], protocolLabel: 'IV', - }, - { - drugName: 'Cefotaxime', category: 'Cephalosporins', testType: 'skin', - presentation: '1g', sptNeatConcentration: 'Neat (100mg/mL)', diluent: '0.9% sodium chloride (reconstitute with 10 mL WFI)', - idtSteps: [s('1:100', '1mg/mL'), s('1:10', '10mg/mL')], - challengeSteps: [], protocolLabel: 'IV', - }, - { - drugName: 'Ceftazidime', category: 'Cephalosporins', testType: 'skin', - presentation: '2g', sptNeatConcentration: 'Neat (100mg/mL)', diluent: '0.9% sodium chloride (reconstitute with 20 mL WFI)', - idtSteps: [s('1:100', '1mg/mL'), s('1:10', '10mg/mL')], - challengeSteps: [], protocolLabel: 'IV', - }, - { - drugName: 'Ceftriaxone', category: 'Cephalosporins', testType: 'skin', - presentation: '1g', sptNeatConcentration: 'Neat (100mg/mL)', diluent: '0.9% sodium chloride (reconstitute with 10 mL WFI)', - idtSteps: [s('1:100', '1mg/mL'), s('1:10', '10mg/mL')], - challengeSteps: [], protocolLabel: 'IV', - }, - { - drugName: 'Cefuroxime', category: 'Cephalosporins', testType: 'skin', - presentation: '750mg', sptNeatConcentration: 'Neat (29mg/mL)', diluent: '0.9% sodium chloride (reconstitute with 30 mL)', - idtSteps: [s('1:100', '0.29mg/mL'), s('1:10', '2.9mg/mL')], - challengeSteps: [], protocolLabel: 'IV', - }, - - // ── HYPNOTICS ───────────────────────────────────────────────────────────── - { - drugName: 'Midazolam', category: 'Hypnotics', testType: 'skin', - presentation: '1mg/mL', sptNeatConcentration: 'Neat (1mg/mL)', diluent: '0.9% sodium chloride', - idtSteps: [s('1:100', '0.01mg/mL')], - challengeSteps: [], protocolLabel: 'IV', - }, - { - drugName: 'Propofol', category: 'Hypnotics', testType: 'skin', - presentation: '10mg/mL', sptNeatConcentration: 'Neat (10mg/mL)', diluent: '0.9% sodium chloride', - idtSteps: [s('1:1,000', '0.01mg/mL'), s('1:10', '1mg/mL')], - challengeSteps: [], protocolLabel: 'IV', - }, - { - drugName: 'Ketamine', category: 'Hypnotics', testType: 'skin', - presentation: '100mg/mL', sptNeatConcentration: 'Neat (100mg/mL)', diluent: '0.9% sodium chloride', - idtSteps: [s('1:1,000', '0.1mg/mL'), s('1:100', '1mg/mL'), s('1:10', '10mg/mL')], - challengeSteps: [], protocolLabel: '1:1,000 start', - }, - { - drugName: 'Ketamine', category: 'Hypnotics', testType: 'skin', - presentation: '100mg/mL', sptNeatConcentration: 'Neat (100mg/mL)', diluent: '0.9% sodium chloride', - idtSteps: [s('1:1,000', '0.1mg/mL')], - challengeSteps: [], protocolLabel: '1:100 start', - }, - { - drugName: 'Thiopental', category: 'Hypnotics', testType: 'skin', - presentation: '25mg/mL', sptNeatConcentration: 'Neat (25mg/mL)', diluent: '0.9% sodium chloride (reconstitute with 20 mL WFI or saline)', - idtSteps: [s('1:1,000', '0.025mg/mL'), s('1:100', '0.25mg/mL'), s('1:10', '2.5mg/mL')], - challengeSteps: [], protocolLabel: '1:1,000 start', - }, - { - drugName: 'Thiopental', category: 'Hypnotics', testType: 'skin', - presentation: '25mg/mL', sptNeatConcentration: 'Neat (25mg/mL)', diluent: '0.9% sodium chloride (reconstitute with 20 mL WFI or saline)', - idtSteps: [s('1:1,000', '0.025mg/mL'), s('1:100', '0.25mg/mL'), s('1:10', '2.5mg/mL')], - challengeSteps: [], protocolLabel: '1:100 start', - }, - - // ── LOCAL ANAESTHETICS ──────────────────────────────────────────────────── - { - drugName: 'Lignocaine', category: 'Local Anaesthetics', testType: 'skin', - presentation: '50mg/5mL', sptNeatConcentration: 'Neat (10mg/mL)', diluent: '0.9% sodium chloride', - idtSteps: [s('1:1,000', '0.01mg/mL'), s('1:100', '0.1mg/mL'), s('1:10', '1mg/mL'), s('Neat', '10mg/mL')], - challengeSteps: [], protocolLabel: 'IV', - }, - { - drugName: 'Mepivacaine', category: 'Local Anaesthetics', testType: 'skin', - presentation: '66mg/2.2mL', sptNeatConcentration: 'Neat (10mg/mL)', diluent: '0.9% sodium chloride (dilute 3% stock with 4.4 mL WFI)', - idtSteps: [s('1:1,000', '0.01mg/mL'), s('1:100', '0.1mg/mL'), s('1:10', '1mg/mL'), s('Neat', '10mg/mL')], - challengeSteps: [], protocolLabel: 'Epidural', - }, - { - drugName: 'Bupivacaine', category: 'Local Anaesthetics', testType: 'skin', - presentation: '50mg/20mL (Epidural)', sptNeatConcentration: 'Neat (2.5mg/mL)', diluent: '0.9% sodium chloride', - idtSteps: [s('1:1,000', '0.0025mg/mL'), s('1:100', '0.025mg/mL'), s('1:10', '0.25mg/mL'), s('Neat', '2.5mg/mL')], - challengeSteps: [], protocolLabel: 'Epidural', - }, - { - drugName: 'Ropivacaine', category: 'Local Anaesthetics', testType: 'skin', - presentation: '40mg/20mL', sptNeatConcentration: 'Neat (2mg/mL)', diluent: '0.9% sodium chloride', - idtSteps: [s('1:1,000', '0.002mg/mL'), s('1:100', '0.02mg/mL'), s('1:10', '0.2mg/mL'), s('Neat', '2mg/mL')], - challengeSteps: [], protocolLabel: 'Epidural Protocol 1', - }, - { - drugName: 'Ropivacaine', category: 'Local Anaesthetics', testType: 'skin', - presentation: '40mg/20mL', sptNeatConcentration: 'Neat (2mg/mL)', diluent: '0.9% sodium chloride', - idtSteps: [s('1:10', '0.2mg/mL'), s('Neat', '2mg/mL')], - challengeSteps: [], protocolLabel: 'Epidural Protocol 2', - }, + // ── MUSCLE RELAXANTS (from SCRATCH snapshot) ──────────────────────────────── + findGenerated('Cis-atracurium', 'skin'), + findGenerated('Rocuronium', 'skin'), + findGenerated('Pancuronium', 'skin'), + findGenerated('Vecuronium', 'skin'), + findGenerated('Suxamethonium', 'skin'), - // ── OPIOIDS ─────────────────────────────────────────────────────────────── - { - drugName: 'Alfentanil', category: 'Opioids', testType: 'skin', - presentation: '1mg/2mL', sptNeatConcentration: 'Neat (0.5mg/mL)', diluent: '0.9% sodium chloride', - idtSteps: [s('1:100', '0.005mg/mL'), s('1:10', '0.05mg/mL')], - challengeSteps: [], protocolLabel: 'IV', - }, - { - drugName: 'Fentanyl', category: 'Opioids', testType: 'skin', - presentation: '100mcg/2mL', sptNeatConcentration: 'Neat (0.05mg/mL)', diluent: '0.9% sodium chloride', - idtSteps: [s('1:100', '0.0005mg/mL'), s('1:10', '0.005mg/mL')], - challengeSteps: [], protocolLabel: 'IV', - }, - { - drugName: 'Morphine', category: 'Opioids', testType: 'skin', - presentation: '10mg/mL', sptNeatConcentration: '1:10 (1mg/mL)', diluent: '0.9% sodium chloride', - idtSteps: [s('1:100,000', '0.0001mg/mL'), s('1:100', '0.0005mg/mL')], - challengeSteps: [], protocolLabel: '1:1,000 start', - }, - { - drugName: 'Morphine', category: 'Opioids', testType: 'skin', - presentation: '10mg/mL', sptNeatConcentration: '1:10 (1mg/mL)', diluent: '0.9% sodium chloride', - idtSteps: [s('1:100,000', '0.0001mg/mL'), s('1:1,000', '0.01mg/mL')], - challengeSteps: [], protocolLabel: '1:100 start', - }, - { - drugName: 'Remifentanil', category: 'Opioids', testType: 'skin', - presentation: '1mg', sptNeatConcentration: 'Neat (0.05mg/mL)', diluent: '0.9% sodium chloride (reconstitute with 20 mL WFI)', - idtSteps: [s('1:100', '0.0005mg/mL'), s('1:10', '0.005mg/mL')], - challengeSteps: [], protocolLabel: '1:1,000 start', - }, - { - drugName: 'Remifentanil', category: 'Opioids', testType: 'skin', - presentation: '1mg', sptNeatConcentration: 'Neat (0.05mg/mL)', diluent: '0.9% sodium chloride (reconstitute with 20 mL WFI)', - idtSteps: [s('1:100', '0.0005mg/mL'), s('1:10', '0.005mg/mL'), s('1:1,000', '0.01mg/mL')], - challengeSteps: [], protocolLabel: '1:100 start', - }, - { - drugName: 'Oxycodone', category: 'Opioids', testType: 'skin', - presentation: '10mg/mL', sptNeatConcentration: 'Neat (10mg/mL)', diluent: '0.9% sodium chloride', - idtSteps: [s('1:1,000', '0.01mg/mL'), s('1:100', '0.1mg/mL')], - challengeSteps: [], protocolLabel: 'IV', - }, + // ── REVERSAL AGENTS & PENICILLINS (DREAM-only) ────────────────────────────── + ...DREAM_ONLY_PROTOCOLS.slice(0, 17), - // ── ANTISEPTICS ─────────────────────────────────────────────────────────── - { - drugName: 'Chlorhexidine', category: 'Antiseptics', testType: 'skin', - presentation: '0.02%', sptNeatConcentration: 'Neat (0.2mg/mL)', diluent: '0.9% sodium chloride', - idtSteps: [s('1:100', '0.002mg/mL')], - challengeSteps: [], protocolLabel: '0.02%', - }, - { - drugName: 'Povidone Iodine', category: 'Antiseptics', testType: 'skin', - presentation: '10% w/v', sptNeatConcentration: 'Neat (100mg/mL)', diluent: '0.9% sodium chloride', - idtSteps: [s('1:10,000', '0.01mg/mL'), s('1:1,000', '0.1mg/mL'), s('1:100', '1mg/mL')], - challengeSteps: [], protocolLabel: '1:1,000 start', - }, - { - drugName: 'Povidone Iodine', category: 'Antiseptics', testType: 'skin', - presentation: '10% w/v', sptNeatConcentration: 'Neat (100mg/mL)', diluent: '0.9% sodium chloride', - idtSteps: [s('1:10,000', '0.01mg/mL'), s('1:1,000', '0.1mg/mL'), s('1:100', '1mg/mL')], - challengeSteps: [], protocolLabel: '1:100 start', - }, + // ── CEPHALOSPORINS: Cefazolin skin (from SCRATCH snapshot) ────────────────── + findGenerated('Cefazolin', 'skin'), - // ── PROTON PUMP INHIBITORS ──────────────────────────────────────────────── - { - drugName: 'Esomeprazole', category: 'Proton Pump Inhibitors', testType: 'skin', - presentation: '20mg', sptNeatConcentration: 'Neat (20mg/mL)', diluent: '0.9% sodium chloride (dissolve in 1 mL)', - idtSteps: [], - challengeSteps: [], protocolLabel: '', - }, - { - drugName: 'Lansoprazole', category: 'Proton Pump Inhibitors', testType: 'skin', - presentation: '30mg', sptNeatConcentration: 'Neat (30mg/mL)', diluent: '0.9% sodium chloride (dissolve in 1 mL)', - idtSteps: [], - challengeSteps: [], protocolLabel: '', - }, - { - drugName: 'Omeprazole', category: 'Proton Pump Inhibitors', testType: 'skin', - presentation: '20mg', sptNeatConcentration: 'Neat (20mg/mL)', diluent: '0.9% sodium chloride (dissolve in 1 mL)', - idtSteps: [], - challengeSteps: [], protocolLabel: '', - }, - { - drugName: 'Pantoprazole', category: 'Proton Pump Inhibitors', testType: 'skin', - presentation: '40mg', sptNeatConcentration: 'Neat (40mg/mL)', diluent: '0.9% sodium chloride (reconstitute with 10 mL)', - idtSteps: [s('1:1,000', '0.04mg/mL'), s('1:100', '0.4mg/mL'), s('1:10', '4mg/mL')], - challengeSteps: [], protocolLabel: 'IV', - }, - { - drugName: 'Rabeprazole', category: 'Proton Pump Inhibitors', testType: 'skin', - presentation: '40mg', sptNeatConcentration: 'Neat (40mg/mL)', diluent: '0.9% sodium chloride (dissolve in 1 mL)', - idtSteps: [], - challengeSteps: [], protocolLabel: '', - }, + // ── REST OF DRUGS (DREAM-only) ────────────────────────────────────────────── + ...DREAM_ONLY_PROTOCOLS.slice(17, 100), - // ── OTHERS (skin testing) ───────────────────────────────────────────────── - { - drugName: 'Actrapid (Insulin)', category: 'Others', testType: 'skin', - presentation: '100units/mL', sptNeatConcentration: 'Neat (100 U/mL)', diluent: '0.9% sodium chloride', - idtSteps: [s('1:20', '5U/mL')], - challengeSteps: [], protocolLabel: 'S/C', - }, - { - drugName: 'Azithromycin', category: 'Others', testType: 'skin', - presentation: '500mg', sptNeatConcentration: 'Neat (100mg/mL)', diluent: '0.9% sodium chloride (reconstitute with 4.8 mL WFI)', - idtSteps: [s('1:10,000', '0.01mg/mL'), s('1:1,000', '0.1mg/mL'), s('1:10', '15mg/mL')], - challengeSteps: [], protocolLabel: 'IV', - }, - { - drugName: 'Betamethasone', category: 'Others', testType: 'experimental', - presentation: '5.7mg/mL', sptNeatConcentration: 'Neat (5.7mg/mL)', diluent: '0.9% sodium chloride', - idtSteps: [s('1:100', '0.057mg/mL'), s('1:15', '0.38mg/mL')], - challengeSteps: [], protocolLabel: 'IV', - }, - { - drugName: 'Cefuroxime Suspension', category: 'Others', testType: 'skin', - needsPharmacyVerification: true, - presentation: '125mg/5mL', sptNeatConcentration: 'Neat (25mg/mL)', diluent: '', - idtSteps: [s('1:100', '0.29mg/mL')], - challengeSteps: [], protocolLabel: 'Suspension', - }, - { - drugName: 'Ciprofloxacin', category: 'Others', testType: 'skin', - presentation: '200mg/100mL', sptNeatConcentration: 'Neat (2mg/mL)', diluent: '0.9% sodium chloride', - idtSteps: [s('1:100', '0.04mg/mL')], - challengeSteps: [], protocolLabel: 'IV', - }, - { - drugName: 'Clindamycin', category: 'Others', testType: 'skin', - presentation: '300mg/2mL', sptNeatConcentration: 'Neat (150mg/mL)', diluent: '0.9% sodium chloride', - idtSteps: [s('1:10', '15mg/mL'), s('1:1,000', '0.005mg/mL')], - challengeSteps: [], protocolLabel: 'IV', - }, - { - drugName: 'Dalteparin', category: 'Others', testType: 'skin', - presentation: '10000U/mL', sptNeatConcentration: 'Neat (10000U/mL)', diluent: '0.9% sodium chloride', - idtSteps: [s('1:1,000', '10U/mL'), s('1:100', '100U/mL'), s('1:10', '1000U/mL')], - challengeSteps: [], protocolLabel: 'SC', - }, - { - drugName: 'Dexamethasone', category: 'Others', testType: 'skin', - presentation: '4mg/mL', sptNeatConcentration: 'Neat (4mg/mL)', diluent: '0.9% sodium chloride', - idtSteps: [s('1:100', '0.04mg/mL'), s('1:10', '0.4mg/mL')], - challengeSteps: [], protocolLabel: 'IV', - }, - { - drugName: 'Doxycycline', category: 'Others', testType: 'skin', - presentation: '100mg', sptNeatConcentration: 'Neat (10mg/mL)', diluent: '0.9% sodium chloride (reconstitute with 10 mL WFI or saline)', - idtSteps: [s('1:10,000', '0.001mg/mL'), s('1:1,000', '0.01mg/mL'), s('1:100', '0.05mg/mL')], - challengeSteps: [], protocolLabel: '1:1,000 start', - }, - { - drugName: 'Doxycycline', category: 'Others', testType: 'skin', - presentation: '100mg', sptNeatConcentration: 'Neat (10mg/mL)', diluent: '0.9% sodium chloride (reconstitute with 10 mL WFI or saline)', - idtSteps: [s('1:10,000', '0.001mg/mL'), s('1:1,000', '0.01mg/mL'), s('1:100', '0.02mg/mL')], - challengeSteps: [], protocolLabel: '1:100 start', - }, - { - drugName: 'Droperidol', category: 'Others', testType: 'skin', - presentation: '10mg/2mL', sptNeatConcentration: 'Neat (5mg/mL)', diluent: '0.9% sodium chloride', - idtSteps: [s('1:1,000', '0.005mg/mL'), s('1:100', '0.05mg/mL')], - challengeSteps: [], protocolLabel: 'IV', - }, - { - drugName: 'Enoxaparin', category: 'Others', testType: 'skin', - presentation: '100mg/mL', sptNeatConcentration: 'Neat (100mg/mL)', diluent: '0.9% sodium chloride', - idtSteps: [s('1:1,000', '0.1mg/mL'), s('1:100', '1mg/mL'), s('1:10', '10mg/mL')], - challengeSteps: [], protocolLabel: 'SC', - }, - { - drugName: 'Fluconazole', category: 'Others', testType: 'skin', - presentation: '100mg/50mL', sptNeatConcentration: '1:10 (0.2mg/mL)', diluent: '0.9% sodium chloride', - idtSteps: [s('1:1,000', '0.002mg/mL'), s('1:100', '0.02mg/mL'), s('1:10', '0.2mg/mL')], - challengeSteps: [], protocolLabel: 'IV', - }, - { - drugName: 'Glycopyrronium', category: 'Others', testType: 'experimental', - presentation: '200mcg/1mL', sptNeatConcentration: 'Neat (0.2mg/mL)', diluent: '0.9% sodium chloride', - idtSteps: [s('1:1,000', '0.0002mg/mL'), s('1:100', '0.057mg/mL')], - challengeSteps: [], protocolLabel: '', - }, - { - drugName: 'Granisetron', category: 'Others', testType: 'skin', - presentation: '3mg/3mL', sptNeatConcentration: 'Neat (1mg/mL)', diluent: '0.9% sodium chloride', - idtSteps: [s('1:100', '0.01mg/mL'), s('1:1,000', '0.001mg/mL')], - challengeSteps: [], protocolLabel: 'IV', - }, - { - drugName: 'Heparin', category: 'Others', testType: 'skin', - presentation: '5000U/mL', sptNeatConcentration: 'Neat (5000U/mL)', diluent: '0.9% sodium chloride', - idtSteps: [s('1:1,000', '5U/mL'), s('1:100', '50U/mL'), s('1:10', '500U/mL')], - challengeSteps: [], protocolLabel: 'SC', - }, - { - drugName: 'Humulin NPH (Insulin)', category: 'Others', testType: 'skin', - presentation: '100units/mL', sptNeatConcentration: 'Neat (100 U/mL)', diluent: '0.9% sodium chloride', - idtSteps: [s('1:20', '5U/mL')], - challengeSteps: [], protocolLabel: 'S/C', - }, - { - drugName: 'Humulin R (Insulin)', category: 'Others', testType: 'skin', - presentation: '100units/mL', sptNeatConcentration: 'Neat (100 U/mL)', diluent: '0.9% sodium chloride', - idtSteps: [s('1:20', '5U/mL')], - challengeSteps: [], protocolLabel: 'S/C', - }, - { - drugName: 'Hydrocortisone', category: 'Others', testType: 'experimental', - presentation: '50mg/mL', sptNeatConcentration: 'Neat (50mg/mL)', diluent: '0.9% sodium chloride (reconstitute with 2 mL WFI or saline)', - idtSteps: [s('1:100', '0.50mg/mL'), s('1:10', '5mg/mL')], - challengeSteps: [], protocolLabel: 'IV', - }, - { - drugName: 'Latex', category: 'Others', testType: 'skin', - presentation: '', sptNeatConcentration: 'Neat', diluent: 'Neat — no diluent', - idtSteps: [], - challengeSteps: [], protocolLabel: '', - }, - { - drugName: 'Levofloxacin', category: 'Others', testType: 'skin', - needsPharmacyVerification: true, - presentation: '500mg', sptNeatConcentration: 'Neat (5mg/mL)', diluent: '0.9% sodium chloride (tablet prep — confirm with pharmacist)', - idtSteps: [s('1:100', '0.05mg/mL')], - challengeSteps: [], protocolLabel: 'Tablet', - }, - { - drugName: 'Levonorgestrel', category: 'Others', testType: 'skin', - needsPharmacyVerification: true, - presentation: '750mcg tablet', sptNeatConcentration: 'Neat', diluent: '0.9% sodium chloride', - idtSteps: [s('1:100', '0.05mg/mL')], - challengeSteps: [], protocolLabel: 'Oral', - }, - { - drugName: 'Medroxyprogesterone', category: 'Others', testType: 'skin', - presentation: '150mg/1mL', sptNeatConcentration: '1:3 (50mg/mL)', diluent: '0.9% sodium chloride', - idtSteps: [s('1:100', '0.05mg/mL'), s('1:10', '5mg/mL')], - challengeSteps: [], protocolLabel: 'Inj', - }, - { - drugName: 'Metacresol', category: 'Others', testType: 'skin', - presentation: '1034mg/mL', sptNeatConcentration: '1.5mg/mL', diluent: '0.9% sodium chloride', - idtSteps: [s('1:20', '0.075mg/mL')], - challengeSteps: [], protocolLabel: '1:1,000 start', - }, - { - drugName: 'Metacresol', category: 'Others', testType: 'skin', - presentation: '1034mg/mL', sptNeatConcentration: '3mg/mL', diluent: '0.9% sodium chloride', - idtSteps: [s('1:20', '0.15mg/mL')], - challengeSteps: [], protocolLabel: '1:100 start', - }, - { - drugName: 'Methylprednisolone', category: 'Others', testType: 'experimental', - presentation: '1g', sptNeatConcentration: 'Neat (20mg/mL)', diluent: '0.9% sodium chloride (reconstitute with 5 mL)', - idtSteps: [s('1:100', '0.2mg/mL'), s('1:10', '2mg/mL'), s('1:1,000', '0.04mg/mL')], - challengeSteps: [], protocolLabel: 'IV', - }, - { - drugName: 'Metoclopramide', category: 'Others', testType: 'skin', - presentation: '10mg/2mL', sptNeatConcentration: 'Neat (5mg/mL)', diluent: '0.9% sodium chloride', - idtSteps: [s('1:1,000', '0.005mg/mL'), s('1:100', '0.05mg/mL')], - challengeSteps: [], protocolLabel: 'IV', - }, - { - drugName: 'Metronidazole', category: 'Others', testType: 'skin', - presentation: '500mg/100mL', sptNeatConcentration: 'Neat (5mg/mL)', diluent: '0.9% sodium chloride', - idtSteps: [s('1:100', '0.05mg/mL'), s('1:1,000', '0.025mg/mL')], - challengeSteps: [], protocolLabel: 'IV', - }, - { - drugName: 'Neostigmine', category: 'Others', testType: 'experimental', - presentation: '2.5mg/1mL', sptNeatConcentration: 'Neat (2.5mg/mL)', diluent: '0.9% sodium chloride', - idtSteps: [s('1:1,000', '0.0025mg/mL'), s('1:100', '0.025mg/mL')], - challengeSteps: [], protocolLabel: 'Inj', - }, - { - drugName: 'Novorapid (Insulin)', category: 'Others', testType: 'skin', - presentation: '100units/mL', sptNeatConcentration: 'Neat (100 U/mL)', diluent: '0.9% sodium chloride', - idtSteps: [s('1:20', '5U/mL')], - challengeSteps: [], protocolLabel: 'S/C', - }, - { - drugName: 'Omnipaque', category: 'Others', testType: 'skin', - presentation: '350mg I/mL', sptNeatConcentration: 'Neat', diluent: '0.9% sodium chloride', - idtSteps: [s('1:100', ''), s('1:10', '')], - challengeSteps: [], protocolLabel: 'IV Contrast', - }, - { - drugName: 'Ondansetron', category: 'Others', testType: 'skin', - presentation: '4mg/2mL', sptNeatConcentration: 'Neat (2mg/mL)', diluent: '0.9% sodium chloride', - idtSteps: [s('1:1,000', '0.002mg/mL'), s('1:100', '0.02mg/mL')], - challengeSteps: [], protocolLabel: 'IV', - }, - { - drugName: 'Optisulin (Insulin)', category: 'Others', testType: 'skin', - presentation: '100units/mL', sptNeatConcentration: 'Neat (100 U/mL)', diluent: '0.9% sodium chloride', - idtSteps: [s('1:20', '5U/mL')], - challengeSteps: [], protocolLabel: 'S/C', - }, - { - drugName: 'Paracetamol', category: 'Others', testType: 'skin', - presentation: '1000mg/100mL', sptNeatConcentration: 'Neat (10mg/mL)', diluent: '0.9% sodium chloride', - idtSteps: [s('1:100', '0.1mg/mL'), s('1:10', '1mg/mL')], - challengeSteps: [], protocolLabel: 'IV', - }, - { - drugName: 'Parecoxib', category: 'Others', testType: 'skin', - presentation: '40mg', sptNeatConcentration: 'Neat (8mg/mL)', diluent: '0.9% sodium chloride (reconstitute with 5 mL)', - idtSteps: [s('1:100', '0.08mg/mL')], - challengeSteps: [], protocolLabel: 'IV', - }, - { - drugName: 'Patent Blue', category: 'Others', testType: 'skin', - presentation: '50mg/2mL', sptNeatConcentration: 'Neat (25mg/mL)', diluent: '0.9% sodium chloride', - idtSteps: [s('1:1,000', '0.025mg/mL'), s('1:100', '0.25mg/mL')], - challengeSteps: [], protocolLabel: 'SC', - }, - { - drugName: 'Protamine', category: 'Others', testType: 'skin', - presentation: '50mg/5mL', sptNeatConcentration: 'Neat (10mg/mL)', diluent: '0.9% sodium chloride', - idtSteps: [s('1:1,000', '0.01mg/mL'), s('0.1:20', '0.05mg/mL')], - challengeSteps: [], protocolLabel: 'IV', - }, - { - drugName: 'Protaphane (Insulin)', category: 'Others', testType: 'skin', - presentation: '100units/mL', sptNeatConcentration: 'Neat (100 U/mL)', diluent: '0.9% sodium chloride', - idtSteps: [s('1:20', '5U/mL')], - challengeSteps: [], protocolLabel: 'S/C', - }, - { - drugName: 'Tranexamic Acid', category: 'Others', testType: 'skin', - presentation: '500mg/5mL', sptNeatConcentration: 'Neat (100mg/mL)', diluent: '0.9% sodium chloride', - idtSteps: [s('1:100', '1mg/mL'), s('1:10', '10mg/mL'), s('1:10,000', '0.01mg/mL')], - challengeSteps: [], protocolLabel: 'IV', - }, - { - drugName: 'Tramadol', category: 'Others', testType: 'experimental', - presentation: '100mg/2mL', sptNeatConcentration: 'Neat (50mg/mL)', diluent: '0.9% sodium chloride', - idtSteps: [s('1:100', '0.5mg/mL')], - challengeSteps: [], protocolLabel: 'IV', - }, - { - drugName: 'Triamcinolone', category: 'Others', testType: 'experimental', - presentation: '40mg/mL', sptNeatConcentration: '1:10 (4mg/mL)', diluent: '0.9% sodium chloride', - idtSteps: [s('1:1,000', '0.04mg/mL'), s('1:100', '0.4mg/mL'), s('1:10', '4mg/mL')], - challengeSteps: [], protocolLabel: 'Inj', - }, - { - drugName: 'Ultravist', category: 'Others', testType: 'skin', - presentation: '46.76g/75mL', sptNeatConcentration: 'Neat', diluent: '0.9% sodium chloride', - idtSteps: [s('1:100', ''), s('1:10', '')], - challengeSteps: [], protocolLabel: 'IV Contrast', - }, - { - drugName: 'Ultravist', category: 'Others', testType: 'control', - presentation: '46.76g/75mL', sptNeatConcentration: 'Neat', diluent: '0.9% sodium chloride', - idtSteps: [s('1:100', ''), s('1:10', '')], - challengeSteps: [], protocolLabel: 'Control', - }, - { - drugName: 'Urografin', category: 'Others', testType: 'skin', - presentation: '', sptNeatConcentration: 'Neat', diluent: '0.9% sodium chloride', - idtSteps: [s('1:100', ''), s('1:10', '')], - challengeSteps: [], protocolLabel: 'IV Contrast', - }, - { - drugName: 'Vancomycin', category: 'Others', testType: 'skin', - presentation: '1g', sptNeatConcentration: 'Neat (100mg/mL)', diluent: '0.9% sodium chloride (reconstitute with 5 mL [500 mg] / 10 mL [1 g])', - idtSteps: [s('1:1,000,000', '0.0001mg/mL'), s('1:100', '0.4mg/mL')], - challengeSteps: [], protocolLabel: 'IV', - }, - { - drugName: 'Visipaque', category: 'Others', testType: 'skin', - presentation: '320mg I/mL', sptNeatConcentration: 'Neat', diluent: '0.9% sodium chloride', - idtSteps: [s('1:100', ''), s('1:10', '')], - challengeSteps: [], protocolLabel: 'IV Contrast', - }, - { - drugName: 'Xylocaine', category: 'Others', testType: 'skin', - presentation: '50mg/5mL', sptNeatConcentration: 'Neat (10mg/mL)', diluent: '0.9% sodium chloride', - idtSteps: [s('1:1,000', '0.01mg/mL'), s('1:100', '0.1mg/mL'), s('1:10', '1mg/mL'), s('Neat', '10mg/mL')], - challengeSteps: [], protocolLabel: 'IV', - }, - // Methylene Blue — not in CSV, keep for backwards compat - { - drugName: 'Methylene Blue', category: 'Others', testType: 'skin', - needsPharmacyVerification: true, - presentation: '', sptNeatConcentration: '', diluent: '', - idtSteps: [], - challengeSteps: [], protocolLabel: '', - }, - // IV Contrast (generic) — not in CSV, keep for backwards compat - { - drugName: 'IV Contrast', category: 'Others', testType: 'skin', - needsPharmacyVerification: true, - presentation: '', sptNeatConcentration: '', diluent: '', - idtSteps: [], - challengeSteps: [], protocolLabel: '', - }, - // Atropine — not in CSV, keep for backwards compat - { - drugName: 'Atropine', category: 'Others', testType: 'skin', - needsPharmacyVerification: true, - presentation: '', sptNeatConcentration: '', diluent: '', - idtSteps: [], - challengeSteps: [], protocolLabel: '', - }, + // ── CEFAZOLIN CHALLENGE (from SCRATCH snapshot) ───────────────────────────── + findGenerated('Cefazolin', 'challenge'), - // ── CHALLENGE / DESENSITISATION ─────────────────────────────────────────── - { - drugName: 'Amoxycillin Suspension', category: 'Others', testType: 'challenge', - presentation: '125mg/5mL', sptNeatConcentration: '', diluent: '', - idtSteps: [], - challengeSteps: [ - c(1, '25mg', '1.0 mL', '25mg'), c(2, '50mg', '2.0 mL', '75mg'), - c(3, '100mg', '4.0 mL', '175mg'), c(4, '250mg', '10 mL', '425mg'), - ], - protocolLabel: 'Oral Graded Challenge', - }, - { - drugName: 'Amoxycillin/Clavulanic Acid', category: 'Others', testType: 'challenge', - presentation: '125mg/31.25mg per 5mL', sptNeatConcentration: '', diluent: '', - idtSteps: [], - challengeSteps: [ - c(1, '25mg/6.25mg', '1 mL', '25mg/6.25mg'), c(2, '125mg/31.25mg', '5 mL', '150mg/37.5mg'), - c(3, '250mg/62.5mg', '10 mL', '400mg/100mg'), c(4, '500mg/125mg', '20 mL', '900mg/225mg'), - ], - protocolLabel: 'Oral Graded Challenge', - }, - { - drugName: 'Cefazolin', category: 'Cephalosporins', testType: 'challenge', - presentation: '1g', sptNeatConcentration: '', diluent: '', - idtSteps: [], - challengeSteps: [ - c(1, '100mg', '10 mL', '100mg'), c(2, '200mg', '20 mL', '300mg'), - c(3, '300mg', '30 mL', '600mg'), c(4, '400mg', '40 mL', '1000mg'), - ], - protocolLabel: 'IV Challenge', - }, - { - drugName: 'Cephalexin', category: 'Penicillins', testType: 'challenge', - presentation: '125mg/5mL suspension', sptNeatConcentration: '', diluent: '', - idtSteps: [], - challengeSteps: [ - c(1, '25mg', '1.0 mL', '25mg'), c(2, '50mg', '2.0 mL', '75mg'), - c(3, '100mg', '4.0 mL', '175mg'), c(4, '250mg', '10 mL', '425mg'), - ], - protocolLabel: 'Oral Graded Challenge', - }, - { - drugName: 'Ciprofloxacin', category: 'Others', testType: 'challenge', - presentation: 'Oral suspension', sptNeatConcentration: '', diluent: '', - idtSteps: [], - challengeSteps: [ - c(1, '50mg', '1 mL', '50mg'), c(2, '125mg', '2.5 mL', '175mg'), - c(3, '250mg', '5 mL', '425mg'), c(4, '500mg', '10 mL', '925mg'), - ], - protocolLabel: 'Oral Graded Challenge', - }, - { - drugName: 'Doxycycline', category: 'Others', testType: 'challenge', - presentation: 'Oral', sptNeatConcentration: '', diluent: '', - idtSteps: [], - challengeSteps: [ - c(1, '10mg', '', ''), c(2, '25mg', '', ''), c(3, '75mg', '', ''), - ], - protocolLabel: 'Oral Graded Challenge', - }, - { - drugName: 'Flucloxacillin', category: 'Penicillins', testType: 'challenge', - presentation: '125mg/5mL suspension', sptNeatConcentration: '', diluent: '', - idtSteps: [], - challengeSteps: [ - c(1, '50mg', '2 mL', '50mg'), c(2, '125mg', '5 mL', '175mg'), c(3, '325mg', '13 mL', '500mg'), - ], - protocolLabel: 'Oral Graded Challenge', - }, - { - drugName: 'Lignocaine', category: 'Local Anaesthetics', testType: 'challenge', - presentation: '1% 50mg/5mL', sptNeatConcentration: '', diluent: '', - idtSteps: [], - challengeSteps: [c(1, '10mg', '1 mL', '')], - protocolLabel: 'Challenge', - }, - { - drugName: 'Meloxicam', category: 'Others', testType: 'challenge', - presentation: 'Tablet', sptNeatConcentration: '', diluent: '', - idtSteps: [], - challengeSteps: [c(1, '2.5mg', '2.5 mL', '2.5mg'), c(2, '5mg', '5 mL', '7.5mg')], - protocolLabel: 'Graded Challenge', - }, - { - drugName: 'Trimethoprim/Sulfamethoxazole', category: 'Others', testType: 'challenge', - presentation: '40mg/200mg per 5mL suspension', sptNeatConcentration: '', diluent: '', - idtSteps: [], - challengeSteps: [ - c(1, '16mg/80mg', '2.0 mL', '16mg/80mg'), c(2, '64mg/320mg', '8.0 mL', '80mg/400mg'), - c(3, '80mg/400mg', '10.0 mL', '160mg/800mg'), - ], - protocolLabel: 'Oral Graded Challenge', - }, - { - drugName: 'Trimethoprim', category: 'Others', testType: 'challenge', - presentation: 'Suspension', sptNeatConcentration: '', diluent: '', - idtSteps: [], - challengeSteps: [ - c(1, '20mg', '2 mL', '20mg'), c(2, '100mg', '10 mL', '120mg'), c(3, '200mg', '20 mL', '320mg'), - ], - protocolLabel: 'Oral Graded Challenge', - }, - { - drugName: 'Voltaren (Diclofenac)', category: 'Others', testType: 'challenge', - presentation: 'Tablet', sptNeatConcentration: '', diluent: '', - idtSteps: [], - challengeSteps: [c(1, '25mg', '1 tablet', '25mg'), c(2, '50mg', '1 tablet', '75mg'), c(3, '100mg', '2 tablets', '175mg')], - protocolLabel: 'Graded Challenge', - }, + // ── REMAINING CHALLENGES (DREAM-only) ─────────────────────────────────────── + ...DREAM_ONLY_PROTOCOLS.slice(100), ]; // ── Category ordering ────────────────────────────────────────────────────── @@ -790,12 +70,12 @@ export const MASTERLIST_CATEGORIES: string[] = [ // ── Helpers ──────────────────────────────────────────────────────────────── export function getProtocolsForDrug(drugName: string): DrugProtocol[] { - return DRUG_MASTERLIST.filter(p => p.drugName === drugName); + return DRUG_MASTERLIST.filter((p) => p.drugName === drugName); } export function getSkinProtocolsForDrug(drugName: string): DrugProtocol[] { return DRUG_MASTERLIST.filter( - p => p.drugName === drugName && (p.testType === 'skin' || p.testType === 'control' || p.testType === 'experimental') + (p) => p.drugName === drugName && (p.testType === 'skin' || p.testType === 'control' || p.testType === 'experimental') ); } diff --git a/src/shared/data/expectedProtocolOrder.json b/src/shared/data/expectedProtocolOrder.json new file mode 100644 index 0000000..c6d3f59 --- /dev/null +++ b/src/shared/data/expectedProtocolOrder.json @@ -0,0 +1,587 @@ +{ + "_comment": "Frozen positional order of DRUG_MASTERLIST as it existed before generation from SCRATCH. DREAM selects protocol variants by array index, so a shift here re-points saved clinical plans at different doses. Captured from origin/main at the Phase 2 cutover. Only change this deliberately, alongside a migration for stored protocolIndex values.", + "capturedFrom": "origin/main @ pre-snapshot cutover", + "count": 116, + "order": [ + { + "drugName": "Cis-atracurium", + "testType": "skin", + "protocolLabel": "IV" + }, + { + "drugName": "Rocuronium", + "testType": "skin", + "protocolLabel": "IV" + }, + { + "drugName": "Pancuronium", + "testType": "skin", + "protocolLabel": "IV" + }, + { + "drugName": "Vecuronium", + "testType": "skin", + "protocolLabel": "IV" + }, + { + "drugName": "Suxamethonium", + "testType": "skin", + "protocolLabel": "IV" + }, + { + "drugName": "Sugammadex (Alone)", + "testType": "skin", + "protocolLabel": "Alone" + }, + { + "drugName": "Sugammadex (+ Rocuronium)", + "testType": "skin", + "protocolLabel": "+ Rocuronium" + }, + { + "drugName": "Penicillin Major", + "testType": "skin", + "protocolLabel": "PPL" + }, + { + "drugName": "Penicillin Minor", + "testType": "skin", + "protocolLabel": "MD" + }, + { + "drugName": "Ampicillin", + "testType": "skin", + "protocolLabel": "Neat SPT" + }, + { + "drugName": "Ampicillin", + "testType": "skin", + "protocolLabel": "1:5 SPT" + }, + { + "drugName": "Ampicillin", + "testType": "control", + "protocolLabel": "Control" + }, + { + "drugName": "Amoxycillin", + "testType": "skin", + "protocolLabel": "Neat SPT" + }, + { + "drugName": "Amoxycillin", + "testType": "skin", + "protocolLabel": "1:5 SPT" + }, + { + "drugName": "Benzylpenicillin", + "testType": "skin", + "protocolLabel": "1:1,000 start" + }, + { + "drugName": "Benzylpenicillin", + "testType": "skin", + "protocolLabel": "1:100 start" + }, + { + "drugName": "Benzylpenicillin", + "testType": "control", + "protocolLabel": "Control" + }, + { + "drugName": "Augmentin", + "testType": "skin", + "protocolLabel": "1:1,000 start" + }, + { + "drugName": "Augmentin", + "testType": "skin", + "protocolLabel": "1:100 start" + }, + { + "drugName": "Cephalexin", + "testType": "skin", + "protocolLabel": "IV" + }, + { + "drugName": "Tazocin", + "testType": "skin", + "protocolLabel": "IV" + }, + { + "drugName": "Methoxybenzylpenicillin", + "testType": "skin", + "protocolLabel": "" + }, + { + "drugName": "Cefazolin", + "testType": "skin", + "protocolLabel": "IV" + }, + { + "drugName": "Cefepime", + "testType": "skin", + "protocolLabel": "IV" + }, + { + "drugName": "Cefotaxime", + "testType": "skin", + "protocolLabel": "IV" + }, + { + "drugName": "Ceftazidime", + "testType": "skin", + "protocolLabel": "IV" + }, + { + "drugName": "Ceftriaxone", + "testType": "skin", + "protocolLabel": "IV" + }, + { + "drugName": "Cefuroxime", + "testType": "skin", + "protocolLabel": "IV" + }, + { + "drugName": "Midazolam", + "testType": "skin", + "protocolLabel": "IV" + }, + { + "drugName": "Propofol", + "testType": "skin", + "protocolLabel": "IV" + }, + { + "drugName": "Ketamine", + "testType": "skin", + "protocolLabel": "1:1,000 start" + }, + { + "drugName": "Ketamine", + "testType": "skin", + "protocolLabel": "1:100 start" + }, + { + "drugName": "Thiopental", + "testType": "skin", + "protocolLabel": "1:1,000 start" + }, + { + "drugName": "Thiopental", + "testType": "skin", + "protocolLabel": "1:100 start" + }, + { + "drugName": "Lignocaine", + "testType": "skin", + "protocolLabel": "IV" + }, + { + "drugName": "Mepivacaine", + "testType": "skin", + "protocolLabel": "Epidural" + }, + { + "drugName": "Bupivacaine", + "testType": "skin", + "protocolLabel": "Epidural" + }, + { + "drugName": "Ropivacaine", + "testType": "skin", + "protocolLabel": "Epidural Protocol 1" + }, + { + "drugName": "Ropivacaine", + "testType": "skin", + "protocolLabel": "Epidural Protocol 2" + }, + { + "drugName": "Alfentanil", + "testType": "skin", + "protocolLabel": "IV" + }, + { + "drugName": "Fentanyl", + "testType": "skin", + "protocolLabel": "IV" + }, + { + "drugName": "Morphine", + "testType": "skin", + "protocolLabel": "1:1,000 start" + }, + { + "drugName": "Morphine", + "testType": "skin", + "protocolLabel": "1:100 start" + }, + { + "drugName": "Remifentanil", + "testType": "skin", + "protocolLabel": "1:1,000 start" + }, + { + "drugName": "Remifentanil", + "testType": "skin", + "protocolLabel": "1:100 start" + }, + { + "drugName": "Oxycodone", + "testType": "skin", + "protocolLabel": "IV" + }, + { + "drugName": "Chlorhexidine", + "testType": "skin", + "protocolLabel": "0.02%" + }, + { + "drugName": "Povidone Iodine", + "testType": "skin", + "protocolLabel": "1:1,000 start" + }, + { + "drugName": "Povidone Iodine", + "testType": "skin", + "protocolLabel": "1:100 start" + }, + { + "drugName": "Esomeprazole", + "testType": "skin", + "protocolLabel": "" + }, + { + "drugName": "Lansoprazole", + "testType": "skin", + "protocolLabel": "" + }, + { + "drugName": "Omeprazole", + "testType": "skin", + "protocolLabel": "" + }, + { + "drugName": "Pantoprazole", + "testType": "skin", + "protocolLabel": "IV" + }, + { + "drugName": "Rabeprazole", + "testType": "skin", + "protocolLabel": "" + }, + { + "drugName": "Actrapid (Insulin)", + "testType": "skin", + "protocolLabel": "S/C" + }, + { + "drugName": "Azithromycin", + "testType": "skin", + "protocolLabel": "IV" + }, + { + "drugName": "Betamethasone", + "testType": "experimental", + "protocolLabel": "IV" + }, + { + "drugName": "Cefuroxime Suspension", + "testType": "skin", + "protocolLabel": "Suspension" + }, + { + "drugName": "Ciprofloxacin", + "testType": "skin", + "protocolLabel": "IV" + }, + { + "drugName": "Clindamycin", + "testType": "skin", + "protocolLabel": "IV" + }, + { + "drugName": "Dalteparin", + "testType": "skin", + "protocolLabel": "SC" + }, + { + "drugName": "Dexamethasone", + "testType": "skin", + "protocolLabel": "IV" + }, + { + "drugName": "Doxycycline", + "testType": "skin", + "protocolLabel": "1:1,000 start" + }, + { + "drugName": "Doxycycline", + "testType": "skin", + "protocolLabel": "1:100 start" + }, + { + "drugName": "Droperidol", + "testType": "skin", + "protocolLabel": "IV" + }, + { + "drugName": "Enoxaparin", + "testType": "skin", + "protocolLabel": "SC" + }, + { + "drugName": "Fluconazole", + "testType": "skin", + "protocolLabel": "IV" + }, + { + "drugName": "Glycopyrronium", + "testType": "experimental", + "protocolLabel": "" + }, + { + "drugName": "Granisetron", + "testType": "skin", + "protocolLabel": "IV" + }, + { + "drugName": "Heparin", + "testType": "skin", + "protocolLabel": "SC" + }, + { + "drugName": "Humulin NPH (Insulin)", + "testType": "skin", + "protocolLabel": "S/C" + }, + { + "drugName": "Humulin R (Insulin)", + "testType": "skin", + "protocolLabel": "S/C" + }, + { + "drugName": "Hydrocortisone", + "testType": "experimental", + "protocolLabel": "IV" + }, + { + "drugName": "Latex", + "testType": "skin", + "protocolLabel": "" + }, + { + "drugName": "Levofloxacin", + "testType": "skin", + "protocolLabel": "Tablet" + }, + { + "drugName": "Levonorgestrel", + "testType": "skin", + "protocolLabel": "Oral" + }, + { + "drugName": "Medroxyprogesterone", + "testType": "skin", + "protocolLabel": "Inj" + }, + { + "drugName": "Metacresol", + "testType": "skin", + "protocolLabel": "1:1,000 start" + }, + { + "drugName": "Metacresol", + "testType": "skin", + "protocolLabel": "1:100 start" + }, + { + "drugName": "Methylprednisolone", + "testType": "experimental", + "protocolLabel": "IV" + }, + { + "drugName": "Metoclopramide", + "testType": "skin", + "protocolLabel": "IV" + }, + { + "drugName": "Metronidazole", + "testType": "skin", + "protocolLabel": "IV" + }, + { + "drugName": "Neostigmine", + "testType": "experimental", + "protocolLabel": "Inj" + }, + { + "drugName": "Novorapid (Insulin)", + "testType": "skin", + "protocolLabel": "S/C" + }, + { + "drugName": "Omnipaque", + "testType": "skin", + "protocolLabel": "IV Contrast" + }, + { + "drugName": "Ondansetron", + "testType": "skin", + "protocolLabel": "IV" + }, + { + "drugName": "Optisulin (Insulin)", + "testType": "skin", + "protocolLabel": "S/C" + }, + { + "drugName": "Paracetamol", + "testType": "skin", + "protocolLabel": "IV" + }, + { + "drugName": "Parecoxib", + "testType": "skin", + "protocolLabel": "IV" + }, + { + "drugName": "Patent Blue", + "testType": "skin", + "protocolLabel": "SC" + }, + { + "drugName": "Protamine", + "testType": "skin", + "protocolLabel": "IV" + }, + { + "drugName": "Protaphane (Insulin)", + "testType": "skin", + "protocolLabel": "S/C" + }, + { + "drugName": "Tranexamic Acid", + "testType": "skin", + "protocolLabel": "IV" + }, + { + "drugName": "Tramadol", + "testType": "experimental", + "protocolLabel": "IV" + }, + { + "drugName": "Triamcinolone", + "testType": "experimental", + "protocolLabel": "Inj" + }, + { + "drugName": "Ultravist", + "testType": "skin", + "protocolLabel": "IV Contrast" + }, + { + "drugName": "Ultravist", + "testType": "control", + "protocolLabel": "Control" + }, + { + "drugName": "Urografin", + "testType": "skin", + "protocolLabel": "IV Contrast" + }, + { + "drugName": "Vancomycin", + "testType": "skin", + "protocolLabel": "IV" + }, + { + "drugName": "Visipaque", + "testType": "skin", + "protocolLabel": "IV Contrast" + }, + { + "drugName": "Xylocaine", + "testType": "skin", + "protocolLabel": "IV" + }, + { + "drugName": "Methylene Blue", + "testType": "skin", + "protocolLabel": "" + }, + { + "drugName": "IV Contrast", + "testType": "skin", + "protocolLabel": "" + }, + { + "drugName": "Atropine", + "testType": "skin", + "protocolLabel": "" + }, + { + "drugName": "Amoxycillin Suspension", + "testType": "challenge", + "protocolLabel": "Oral Graded Challenge" + }, + { + "drugName": "Amoxycillin/Clavulanic Acid", + "testType": "challenge", + "protocolLabel": "Oral Graded Challenge" + }, + { + "drugName": "Cefazolin", + "testType": "challenge", + "protocolLabel": "IV Challenge" + }, + { + "drugName": "Cephalexin", + "testType": "challenge", + "protocolLabel": "Oral Graded Challenge" + }, + { + "drugName": "Ciprofloxacin", + "testType": "challenge", + "protocolLabel": "Oral Graded Challenge" + }, + { + "drugName": "Doxycycline", + "testType": "challenge", + "protocolLabel": "Oral Graded Challenge" + }, + { + "drugName": "Flucloxacillin", + "testType": "challenge", + "protocolLabel": "Oral Graded Challenge" + }, + { + "drugName": "Lignocaine", + "testType": "challenge", + "protocolLabel": "Challenge" + }, + { + "drugName": "Meloxicam", + "testType": "challenge", + "protocolLabel": "Graded Challenge" + }, + { + "drugName": "Trimethoprim/Sulfamethoxazole", + "testType": "challenge", + "protocolLabel": "Oral Graded Challenge" + }, + { + "drugName": "Trimethoprim", + "testType": "challenge", + "protocolLabel": "Oral Graded Challenge" + }, + { + "drugName": "Voltaren (Diclofenac)", + "testType": "challenge", + "protocolLabel": "Graded Challenge" + } + ] +} diff --git a/src/shared/data/protocols.snapshot.json b/src/shared/data/protocols.snapshot.json new file mode 100644 index 0000000..1b3354f --- /dev/null +++ b/src/shared/data/protocols.snapshot.json @@ -0,0 +1,259 @@ +{ + "schema_version": "1.0", + "generated_at": "2026-08-20T09:16:09Z", + "source_commit": "6d12cec", + "drugs": [ + { + "slug": "cefazolin", + "title": "Cefazolin", + "version": "1.2", + "last_reviewed": "2026-03-28", + "dream": { + "category": "Cephalosporins" + }, + "protocols": [ + { + "id": "iv", + "label": "IV", + "test_type": "skin", + "presentation": "1 g powder for injection", + "diluent": "0.9% sodium chloride (reconstitute with 10 mL WFI)", + "spt": { + "dilution": "Neat", + "concentration": "100 mg/mL", + "positive_control": "Histamine 10 mg/mL", + "negative_control": "Normal saline" + }, + "idt": [ + { + "dilution": "1:100", + "concentration": "1 mg/mL", + "preparation": "0.1 mL of 10 mg/mL + 0.9 mL NS" + }, + { + "dilution": "1:10", + "concentration": "10 mg/mL", + "preparation": "0.1 mL neat + 0.9 mL NS" + } + ], + "challenge": { + "interval": "15 min", + "steps": [ + { + "dose": "100 mg" + }, + { + "dose": "200 mg" + }, + { + "dose": "300 mg" + }, + { + "dose": "400 mg", + "interval": "60 min observation" + } + ] + }, + "under_review": false, + "review_note": "", + "needs_pharmacy_verification": false + } + ] + }, + { + "slug": "cis-atracurium", + "title": "Cis-atracurium", + "version": "1.3", + "last_reviewed": "2026-08-20", + "dream": { + "category": "Muscle Relaxants" + }, + "protocols": [ + { + "id": "iv", + "label": "IV", + "test_type": "skin", + "presentation": "5 mg/2.5 mL (2 mg/mL)", + "diluent": "0.9% sodium chloride", + "spt": { + "dilution": "Neat", + "concentration": "2 mg/mL", + "positive_control": "Histamine 10 mg/mL", + "negative_control": "Normal saline" + }, + "idt": [ + { + "dilution": "1:1,000", + "concentration": "0.002 mg/mL", + "preparation": "0.1 mL of 0.02 mg/mL + 0.9 mL NS" + } + ], + "under_review": false, + "review_note": "", + "needs_pharmacy_verification": false + } + ] + }, + { + "slug": "pancuronium", + "title": "Pancuronium", + "version": "1.3", + "last_reviewed": "2026-08-20", + "dream": { + "category": "Muscle Relaxants" + }, + "protocols": [ + { + "id": "iv", + "label": "IV", + "test_type": "skin", + "presentation": "4 mg/2 mL (2 mg/mL)", + "diluent": "0.9% sodium chloride", + "spt": { + "dilution": "Neat", + "concentration": "2 mg/mL", + "positive_control": "Histamine 10 mg/mL", + "negative_control": "Normal saline" + }, + "idt": [ + { + "dilution": "1:1,000", + "concentration": "0.002 mg/mL", + "preparation": "0.1 mL of 0.02 mg/mL + 0.9 mL NS" + }, + { + "dilution": "1:100", + "concentration": "0.02 mg/mL", + "preparation": "0.1 mL of 0.2 mg/mL + 0.9 mL NS" + } + ], + "under_review": false, + "review_note": "", + "needs_pharmacy_verification": false + } + ] + }, + { + "slug": "rocuronium", + "title": "Rocuronium", + "version": "1.3", + "last_reviewed": "2026-08-20", + "dream": { + "category": "Muscle Relaxants" + }, + "protocols": [ + { + "id": "iv", + "label": "IV", + "test_type": "skin", + "presentation": "50 mg/5 mL (10 mg/mL)", + "diluent": "0.9% sodium chloride", + "spt": { + "dilution": "Neat", + "concentration": "10 mg/mL", + "positive_control": "Histamine 10 mg/mL", + "negative_control": "Normal saline" + }, + "idt": [ + { + "dilution": "1:1,000", + "concentration": "0.01 mg/mL", + "preparation": "0.1 mL of 0.1 mg/mL + 0.9 mL NS" + }, + { + "dilution": "1:100", + "concentration": "0.1 mg/mL", + "preparation": "0.1 mL of 1 mg/mL + 0.9 mL NS" + } + ], + "under_review": false, + "review_note": "", + "needs_pharmacy_verification": false + } + ] + }, + { + "slug": "suxamethonium", + "title": "Suxamethonium", + "version": "1.3", + "last_reviewed": "2026-08-20", + "dream": { + "category": "Muscle Relaxants" + }, + "protocols": [ + { + "id": "iv", + "label": "IV", + "test_type": "skin", + "presentation": "100 mg/2 mL (50 mg/mL)", + "diluent": "0.9% sodium chloride", + "spt": { + "dilution": "1:5", + "concentration": "10 mg/mL", + "positive_control": "Histamine 10 mg/mL", + "negative_control": "Normal saline" + }, + "idt": [ + { + "dilution": "1:50,000", + "concentration": "0.001 mg/mL", + "preparation": "0.1 mL of 0.01 mg/mL + 0.9 mL NS" + }, + { + "dilution": "1:5,000", + "concentration": "0.01 mg/mL", + "preparation": "0.1 mL of 0.1 mg/mL + 0.9 mL NS" + }, + { + "dilution": "1:500", + "concentration": "0.1 mg/mL", + "preparation": "0.1 mL of 1 mg/mL + 0.9 mL NS" + } + ], + "under_review": false, + "review_note": "", + "needs_pharmacy_verification": false + } + ] + }, + { + "slug": "vecuronium", + "title": "Vecuronium", + "version": "1.3", + "last_reviewed": "2026-08-20", + "dream": { + "category": "Muscle Relaxants" + }, + "protocols": [ + { + "id": "iv", + "label": "IV", + "test_type": "skin", + "presentation": "10 mg powder for injection", + "diluent": "0.9% sodium chloride (reconstitute with 2.5 mL WFI)", + "spt": { + "dilution": "Neat", + "concentration": "4 mg/mL", + "positive_control": "Histamine 10 mg/mL", + "negative_control": "Normal saline" + }, + "idt": [ + { + "dilution": "1:1,000", + "concentration": "0.004 mg/mL", + "preparation": "0.1 mL of 0.04 mg/mL + 0.9 mL NS" + }, + { + "dilution": "1:100", + "concentration": "0.04 mg/mL", + "preparation": "0.1 mL of 0.4 mg/mL + 0.9 mL NS" + } + ], + "under_review": false, + "review_note": "", + "needs_pharmacy_verification": false + } + ] + } + ] +}