@pharmatools/drug-data 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,52 @@
1
+ # @pharmatools/drug-data
2
+
3
+ Shared drug-data engine behind [Pharmonym](https://www.pharmatools.ai/pharmonym)
4
+ and [PubCrawl](https://www.pharmatools.ai/pubcrawl). Deterministic, sourced,
5
+ no AI.
6
+
7
+ - **Name resolution** — brand ⇄ generic, drug class and indications via
8
+ RxNorm/RxNav, with openFDA fallback.
9
+ - **Labels** — dosage, warnings, contraindications, indications and adverse
10
+ reactions from the US FDA label (openFDA, cited to DailyMed) and the UK/EU
11
+ SmPC (eMC / medicines.org.uk).
12
+ - **Comparison** — equivalent US ↔ UK label sections mapped side by side.
13
+
14
+ Pure fetch + parse: no caching, no secrets. Wraps public government APIs.
15
+ Dual-built (ESM + CJS), global `fetch`, Node ≥18.
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ npm install @pharmatools/drug-data
21
+ ```
22
+
23
+ ## Usage
24
+
25
+ ```ts
26
+ import { resolveName, getLabels, compareLabels } from "@pharmatools/drug-data";
27
+
28
+ await resolveName("Lipitor");
29
+ // { inputType: "brand", genericName: "Atorvastatin", drugClass: "...", ... }
30
+
31
+ await getLabels("atorvastatin");
32
+ // { us: { dosage, warnings, ... , sourceUrl }, uk: { ... } }
33
+
34
+ await compareLabels("atorvastatin");
35
+ // { drug, comparisons: [{ topic, us, uk }], usSource, ukSource }
36
+ ```
37
+
38
+ CommonJS consumers use `require("@pharmatools/drug-data")` — same API.
39
+
40
+ ## Develop
41
+
42
+ ```bash
43
+ npm install
44
+ npm test # vitest (mocked fixtures, no network)
45
+ npm run build # tsup -> dist/{index.js,index.cjs,index.d.ts}
46
+ ```
47
+
48
+ ## Sources
49
+
50
+ RxNorm/RxNav, openFDA, DailyMed, eMC (medicines.org.uk). Not a substitute for
51
+ professional medical advice; always verify against the full prescribing
52
+ information.
package/dist/index.cjs ADDED
@@ -0,0 +1,436 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ SECTION_MAP: () => SECTION_MAP,
34
+ cleanSection: () => cleanSection,
35
+ compareLabels: () => compareLabels,
36
+ formatFdaDate: () => formatFdaDate,
37
+ getLabels: () => getLabels,
38
+ getSmpc: () => getSmpc,
39
+ getUspi: () => getUspi,
40
+ resolveName: () => resolveName
41
+ });
42
+ module.exports = __toCommonJS(index_exports);
43
+
44
+ // src/fetch.ts
45
+ var DEFAULT_TIMEOUT_MS = 12e3;
46
+ var MAX_FIELD_CHARS = 1400;
47
+ var USER_AGENT = "drug-data/0.1 (pharmatools.ai; nick@pharmatools.ai)";
48
+ async function timedFetch(url, opts = {}, timeoutMs = DEFAULT_TIMEOUT_MS) {
49
+ const controller = new AbortController();
50
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
51
+ try {
52
+ return await fetch(url, {
53
+ ...opts,
54
+ signal: controller.signal,
55
+ headers: { "User-Agent": USER_AGENT, ...opts.headers || {} }
56
+ });
57
+ } finally {
58
+ clearTimeout(timer);
59
+ }
60
+ }
61
+ async function getJson(url, timeoutMs = DEFAULT_TIMEOUT_MS) {
62
+ try {
63
+ const res = await timedFetch(url, {}, timeoutMs);
64
+ if (!res.ok) return null;
65
+ return await res.json();
66
+ } catch {
67
+ return null;
68
+ }
69
+ }
70
+ function cleanSection(value, maxChars = MAX_FIELD_CHARS) {
71
+ if (!value) return "";
72
+ let text = Array.isArray(value) ? value.join("\n\n") : String(value);
73
+ text = text.replace(/\r/g, "").replace(/[ \t]+/g, " ").replace(/\n{3,}/g, "\n\n").trim();
74
+ text = text.replace(/^\s*\d+(\.\d+)*\s+[A-Z][A-Z \-/,&]+\n+/, "").trim();
75
+ if (text.length > maxChars) {
76
+ const slice = text.slice(0, maxChars);
77
+ const lastStop = Math.max(slice.lastIndexOf(". "), slice.lastIndexOf("\n"));
78
+ text = (lastStop > maxChars * 0.6 ? slice.slice(0, lastStop + 1) : slice).trim() + " \u2026";
79
+ }
80
+ return text;
81
+ }
82
+ function formatFdaDate(yyyymmdd) {
83
+ if (!yyyymmdd || !/^\d{8}$/.test(yyyymmdd)) return "";
84
+ const d = /* @__PURE__ */ new Date(`${yyyymmdd.slice(0, 4)}-${yyyymmdd.slice(4, 6)}-${yyyymmdd.slice(6, 8)}`);
85
+ if (isNaN(d.getTime())) return "";
86
+ return d.toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric" });
87
+ }
88
+ function titleCase(s) {
89
+ return String(s || "").replace(/\w\S*/g, (t) => t.charAt(0).toUpperCase() + t.slice(1).toLowerCase());
90
+ }
91
+ function uniq(arr) {
92
+ return [...new Set(arr.filter(Boolean))];
93
+ }
94
+
95
+ // src/names.ts
96
+ var RXNAV = "https://rxnav.nlm.nih.gov/REST";
97
+ var OPENFDA = "https://api.fda.gov/drug/label.json";
98
+ var TIMEOUT_MS = 1e4;
99
+ var REGION_NOTE = "Brand names shown are those registered in the United States.";
100
+ async function findRxcui(name) {
101
+ const exact = await getJson(`${RXNAV}/rxcui.json?name=${encodeURIComponent(name)}`, TIMEOUT_MS);
102
+ const id = exact?.idGroup?.rxnormId?.[0];
103
+ if (id) return id;
104
+ const approx = await getJson(
105
+ `${RXNAV}/approximateTerm.json?term=${encodeURIComponent(name)}&maxEntries=1`,
106
+ TIMEOUT_MS
107
+ );
108
+ const cand = approx?.approximateGroup?.candidate?.[0];
109
+ return cand ? cand.rxcui : null;
110
+ }
111
+ function collectNames(allRelated, tty) {
112
+ const groups = allRelated?.allRelatedGroup?.conceptGroup ?? [];
113
+ const g = groups.find((x) => x.tty === tty);
114
+ if (!g || !g.conceptProperties) return [];
115
+ return g.conceptProperties.map((c) => c.name);
116
+ }
117
+ async function getSingleIngredientBrands(ingredientRxcui) {
118
+ const data = await getJson(`${RXNAV}/rxcui/${ingredientRxcui}/related.json?tty=SBDF`, TIMEOUT_MS);
119
+ const groups = data?.relatedGroup?.conceptGroup ?? [];
120
+ const g = groups.find((x) => x.tty === "SBDF");
121
+ if (!g || !g.conceptProperties) return [];
122
+ const brands = [];
123
+ for (const c of g.conceptProperties) {
124
+ const m = c.name.match(/\[([^\]]+)\]\s*$/);
125
+ if (!m) continue;
126
+ const prefix = c.name.slice(0, c.name.lastIndexOf("["));
127
+ if (prefix.includes("/")) continue;
128
+ brands.push(m[1].trim());
129
+ }
130
+ return uniq(brands).sort();
131
+ }
132
+ async function getDrugClassAndUses(rxcui) {
133
+ const data = await getJson(
134
+ `${RXNAV}/rxclass/class/byRxcui.json?rxcui=${rxcui}&relaSource=MEDRT`,
135
+ TIMEOUT_MS
136
+ );
137
+ const items = data?.rxclassDrugInfoList?.rxclassDrugInfo ?? [];
138
+ const byType = (t) => {
139
+ const hit = items.find((i) => i.rxclassMinConceptItem.classType === t);
140
+ return hit ? hit.rxclassMinConceptItem.className : null;
141
+ };
142
+ const drugClass = byType("EPC") || byType("MOA") || byType("CHEM");
143
+ const uses = uniq(
144
+ items.filter((i) => i.rela === "may_treat").map((i) => i.rxclassMinConceptItem.className)
145
+ ).slice(0, 6);
146
+ return { drugClass, uses };
147
+ }
148
+ async function resolveViaRxNorm(name) {
149
+ const rxcui = await findRxcui(name);
150
+ if (!rxcui) return null;
151
+ const [props, allRelated] = await Promise.all([
152
+ getJson(`${RXNAV}/rxcui/${rxcui}/properties.json`, TIMEOUT_MS),
153
+ getJson(`${RXNAV}/rxcui/${rxcui}/allrelated.json`, TIMEOUT_MS)
154
+ ]);
155
+ const tty = props?.properties?.tty;
156
+ const canonicalName = props?.properties?.name ?? name;
157
+ if (!tty) return null;
158
+ const inNames = collectNames(allRelated, "IN");
159
+ const minNames = collectNames(allRelated, "MIN");
160
+ const isBrand = tty === "BN" || tty === "SBD" || tty === "BPCK";
161
+ const inputType = isBrand ? "brand" : "generic";
162
+ let brandNames = null;
163
+ if (!isBrand) {
164
+ const single = await getSingleIngredientBrands(rxcui);
165
+ const fallback = uniq([...collectNames(allRelated, "BN"), ...collectNames(allRelated, "BPCK")]).sort();
166
+ brandNames = single.length ? single : fallback;
167
+ }
168
+ let genericName = null;
169
+ if (minNames.length) genericName = titleCase(minNames[0]);
170
+ else if (inNames.length === 1) genericName = titleCase(inNames[0]);
171
+ else if (inNames.length > 1) genericName = titleCase(uniq(inNames).join(" / "));
172
+ const classRxcui = !isBrand ? rxcui : (() => {
173
+ const inGroup = (allRelated?.allRelatedGroup?.conceptGroup ?? []).find((x) => x.tty === "IN");
174
+ return inGroup?.conceptProperties?.[0]?.rxcui ?? rxcui;
175
+ })();
176
+ const { drugClass, uses } = await getDrugClassAndUses(classRxcui);
177
+ return {
178
+ inputType,
179
+ inputName: titleCase(canonicalName),
180
+ genericName,
181
+ brandNames,
182
+ drugClass,
183
+ uses,
184
+ rxcui,
185
+ source: "RxNorm (NLM)",
186
+ regionNote: REGION_NOTE
187
+ };
188
+ }
189
+ async function resolveViaOpenFda(name) {
190
+ const q = name.replace(/"/g, "");
191
+ const search = `(openfda.brand_name:"${q}"+OR+openfda.generic_name:"${q}")`;
192
+ const url = `${OPENFDA}?search=${encodeURIComponent(search).replace(/%2B/g, "+")}&limit=1`;
193
+ const data = await getJson(url, TIMEOUT_MS);
194
+ const fda = data?.results?.[0]?.openfda;
195
+ if (!fda) return null;
196
+ const brand = (fda.brand_name ?? [])[0] ?? "";
197
+ const generic = (fda.generic_name ?? [])[0] ?? "";
198
+ if (!brand && !generic) return null;
199
+ const isBrand = brand.toLowerCase() === name.toLowerCase();
200
+ return {
201
+ inputType: isBrand ? "brand" : "generic",
202
+ inputName: titleCase(name),
203
+ genericName: generic ? titleCase(generic) : null,
204
+ brandNames: isBrand ? null : uniq(fda.brand_name ?? []).sort(),
205
+ drugClass: (fda.pharm_class_epc ?? [])[0] ?? (fda.pharm_class_moa ?? [])[0] ?? null,
206
+ uses: [],
207
+ rxcui: null,
208
+ source: "openFDA",
209
+ regionNote: REGION_NOTE
210
+ };
211
+ }
212
+ async function resolveName(name) {
213
+ if (!name || !name.trim()) return null;
214
+ const clean = name.trim();
215
+ const viaRx = await resolveViaRxNorm(clean).catch(() => null);
216
+ if (viaRx && (viaRx.genericName || viaRx.brandNames && viaRx.brandNames.length)) {
217
+ return viaRx;
218
+ }
219
+ return await resolveViaOpenFda(clean).catch(() => null);
220
+ }
221
+
222
+ // src/labels.ts
223
+ var cheerio = __toESM(require("cheerio"), 1);
224
+ var OPENFDA_URL = "https://api.fda.gov/drug/label.json";
225
+ var EMC_BASE = "https://www.medicines.org.uk";
226
+ async function getUspi(drug) {
227
+ if (!drug) return null;
228
+ const q = drug.trim().replace(/"/g, "");
229
+ const search = `(openfda.generic_name:"${q}"+OR+openfda.brand_name:"${q}")`;
230
+ const url = `${OPENFDA_URL}?search=${encodeURIComponent(search).replace(/%2B/g, "+")}&sort=effective_time:desc&limit=5`;
231
+ const data = await getJson(url);
232
+ const results = data?.results ?? [];
233
+ if (results.length === 0) return null;
234
+ const ranked = results.slice().sort((a, b) => score(b, q) - score(a, q));
235
+ const r = ranked[0];
236
+ const dosage = cleanSection(r.dosage_and_administration);
237
+ const boxed = cleanSection(r.boxed_warning);
238
+ const warn = cleanSection(r.warnings_and_cautions ?? r.warnings);
239
+ const warnings = [boxed ? `BOXED WARNING \u2014 ${boxed}` : "", warn].filter(Boolean).join("\n\n");
240
+ const adverse = cleanSection(r.adverse_reactions);
241
+ const indications = cleanSection(r.indications_and_usage);
242
+ const contraindications = cleanSection(r.contraindications);
243
+ if (!dosage && !warnings && !adverse && !indications && !contraindications) return null;
244
+ const setId = r.openfda?.spl_set_id?.[0] ?? "";
245
+ const brand = r.openfda?.brand_name?.[0] ?? "";
246
+ const generic = r.openfda?.generic_name?.[0] ?? q;
247
+ return {
248
+ region: "US",
249
+ authority: "FDA Prescribing Information",
250
+ productName: brand ? `${brand} (${generic})` : generic,
251
+ indications: indications || null,
252
+ dosage: dosage || null,
253
+ contraindications: contraindications || null,
254
+ warnings: warnings || null,
255
+ adverse: adverse || null,
256
+ updated: formatFdaDate(r.effective_time),
257
+ sourceUrl: setId ? `https://dailymed.nlm.nih.gov/dailymed/drugInfo.cfm?setid=${setId}` : "https://dailymed.nlm.nih.gov/dailymed/",
258
+ sourceLabel: "DailyMed (U.S. National Library of Medicine)"
259
+ };
260
+ }
261
+ function score(r, q) {
262
+ const g = (r.openfda?.generic_name ?? []).join(", ").toLowerCase();
263
+ let s = 0;
264
+ if (g && g.split(/,| and /).length === 1) s += 2;
265
+ if (g.includes(q.toLowerCase())) s += 1;
266
+ return s;
267
+ }
268
+ async function searchEmc(drug) {
269
+ const url = `${EMC_BASE}/emc/search?${new URLSearchParams({ q: drug })}`;
270
+ const res = await timedFetch(url);
271
+ if (!res.ok) return [];
272
+ const html = await res.text();
273
+ const $ = cheerio.load(html);
274
+ const out = [];
275
+ $("a[href*='/emc/product/']").each((_, el) => {
276
+ const href = $(el).attr("href") || "";
277
+ const m = href.match(/\/emc\/product\/(\d+)\/smpc/);
278
+ if (!m) return;
279
+ const name = $(el).text().trim();
280
+ if (!name || name.toLowerCase().includes("health professional")) return;
281
+ if (out.some((x) => x.product_id === m[1])) return;
282
+ out.push({ product_id: m[1], name });
283
+ });
284
+ return out;
285
+ }
286
+ var SMPC_WANT = {
287
+ "4.1": "indications",
288
+ "4.2": "dosage",
289
+ "4.3": "contraindications",
290
+ "4.4": "warnings",
291
+ "4.8": "adverse"
292
+ };
293
+ var SMPC_SECTION_NAMES = {
294
+ "4.1": "Therapeutic indications",
295
+ "4.2": "Posology and method of administration",
296
+ "4.3": "Contraindications",
297
+ "4.4": "Special warnings and precautions for use",
298
+ "4.5": "Interaction with other medicinal products",
299
+ "4.6": "Fertility, pregnancy and lactation",
300
+ "4.7": "Effects on ability to drive and use machines",
301
+ "4.8": "Undesirable effects",
302
+ "4.9": "Overdose",
303
+ "5.1": "Pharmacodynamic properties",
304
+ "5.2": "Pharmacokinetic properties",
305
+ "5.3": "Preclinical safety data"
306
+ };
307
+ function htmlToText(html) {
308
+ const $ = cheerio.load(html);
309
+ $("br").replaceWith("\n");
310
+ $("li").each((_, el) => {
311
+ $(el).prepend("- ");
312
+ $(el).append("\n");
313
+ });
314
+ $("p, div, tr, h1, h2, h3, h4, h5, h6").each((_, el) => {
315
+ $(el).append("\n");
316
+ });
317
+ return $.text().replace(/\n{3,}/g, "\n\n").trim();
318
+ }
319
+ function parseSmpcSections(html) {
320
+ const $ = cheerio.load(html);
321
+ const sectionElements = [];
322
+ $("[id*='SECTION'], [id*='section'], .sectionHeading, h2, h3, h4").each((_, el) => {
323
+ const text = $(el).text().trim();
324
+ const match = text.match(/^(\d+\.?\d*)\s+(.+)/);
325
+ if (match) sectionElements.push({ code: match[1], title: match[2].trim(), element: $(el) });
326
+ });
327
+ if (sectionElements.length === 0) {
328
+ $("*").each((_, el) => {
329
+ const $el = $(el);
330
+ if ($el.children().length > 0 && !$el.is("a, span, strong, em, b, i")) return;
331
+ const text = $el.text().trim();
332
+ const match = text.match(/^(\d+\.?\d*)\s+(.+)/);
333
+ if (match && SMPC_SECTION_NAMES[match[1]]) {
334
+ sectionElements.push({ code: match[1], title: match[2].trim(), element: $el });
335
+ }
336
+ });
337
+ }
338
+ const fields = {};
339
+ for (let i = 0; i < sectionElements.length; i++) {
340
+ const current = sectionElements[i];
341
+ const field = SMPC_WANT[current.code];
342
+ if (!field) continue;
343
+ const next = sectionElements[i + 1];
344
+ let content = "";
345
+ let node = current.element.next();
346
+ while (node.length > 0) {
347
+ if (next && node.is(next.element)) break;
348
+ const nodeHtml = $.html(node);
349
+ if (nodeHtml) content += htmlToText(nodeHtml) + "\n";
350
+ node = node.next();
351
+ }
352
+ if (!content.trim()) {
353
+ const parentHtml = $.html(current.element.parent());
354
+ if (parentHtml) {
355
+ content = htmlToText(parentHtml).replace(current.element.text().trim(), "").trim();
356
+ }
357
+ }
358
+ fields[field] = cleanSection(content);
359
+ }
360
+ return fields;
361
+ }
362
+ async function getSmpc(drug) {
363
+ if (!drug) return null;
364
+ try {
365
+ const matches = await searchEmc(drug);
366
+ if (matches.length === 0) return null;
367
+ const q = drug.toLowerCase();
368
+ matches.sort(
369
+ (a, b) => (b.name.toLowerCase().includes(q) ? 1 : 0) - (a.name.toLowerCase().includes(q) ? 1 : 0)
370
+ );
371
+ const best = matches[0];
372
+ const res = await timedFetch(`${EMC_BASE}/emc/product/${best.product_id}/smpc`);
373
+ if (!res.ok) return null;
374
+ const html = await res.text();
375
+ const sec = parseSmpcSections(html);
376
+ if (!sec.dosage && !sec.warnings && !sec.adverse && !sec.indications && !sec.contraindications)
377
+ return null;
378
+ return {
379
+ region: "UK/EU",
380
+ authority: "Summary of Product Characteristics (SmPC)",
381
+ productName: best.name,
382
+ indications: sec.indications || null,
383
+ dosage: sec.dosage || null,
384
+ contraindications: sec.contraindications || null,
385
+ warnings: sec.warnings || null,
386
+ adverse: sec.adverse || null,
387
+ updated: "",
388
+ sourceUrl: `${EMC_BASE}/emc/product/${best.product_id}/smpc`,
389
+ sourceLabel: "electronic medicines compendium (emc), UK"
390
+ };
391
+ } catch {
392
+ return null;
393
+ }
394
+ }
395
+ async function getLabels(drug) {
396
+ const [us, uk] = await Promise.all([
397
+ getUspi(drug).catch(() => null),
398
+ getSmpc(drug).catch(() => null)
399
+ ]);
400
+ return { us, uk };
401
+ }
402
+
403
+ // src/compare.ts
404
+ var SECTION_MAP = [
405
+ { topic: "Indications", field: "indications" },
406
+ { topic: "Dosing", field: "dosage" },
407
+ { topic: "Contraindications", field: "contraindications" },
408
+ { topic: "Warnings", field: "warnings" },
409
+ { topic: "Adverse Reactions", field: "adverse" }
410
+ ];
411
+ async function compareLabels(drug) {
412
+ const { us, uk } = await getLabels(drug);
413
+ const comparisons = SECTION_MAP.map(({ topic, field }) => ({
414
+ topic,
415
+ us: us ? us[field] : null,
416
+ uk: uk ? uk[field] : null
417
+ })).filter((c) => c.us || c.uk);
418
+ return {
419
+ drug,
420
+ comparisons,
421
+ usSource: us?.sourceUrl ?? null,
422
+ ukSource: uk?.sourceUrl ?? null
423
+ };
424
+ }
425
+ // Annotate the CommonJS export names for ESM import in node:
426
+ 0 && (module.exports = {
427
+ SECTION_MAP,
428
+ cleanSection,
429
+ compareLabels,
430
+ formatFdaDate,
431
+ getLabels,
432
+ getSmpc,
433
+ getUspi,
434
+ resolveName
435
+ });
436
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/fetch.ts","../src/names.ts","../src/labels.ts","../src/compare.ts"],"sourcesContent":["// @pharmatools/drug-data — public surface\n\nexport { resolveName } from \"./names.js\";\nexport { getUspi, getSmpc, getLabels } from \"./labels.js\";\nexport { compareLabels, SECTION_MAP } from \"./compare.js\";\nexport { cleanSection, formatFdaDate } from \"./fetch.js\";\nexport type {\n Region,\n RegionLabel,\n LabelResult,\n ResolveResult,\n LabelComparison,\n CompareResult,\n} from \"./types.js\";\n","/**\n * Shared HTTP + text helpers. Uses the runtime's global `fetch` (Node >=18),\n * so the package has no node-fetch dependency.\n */\n\nconst DEFAULT_TIMEOUT_MS = 12000;\nconst MAX_FIELD_CHARS = 1400;\n\nexport const USER_AGENT = \"drug-data/0.1 (pharmatools.ai; nick@pharmatools.ai)\";\n\nexport async function timedFetch(\n url: string,\n opts: RequestInit = {},\n timeoutMs = DEFAULT_TIMEOUT_MS\n): Promise<Response> {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n try {\n return await fetch(url, {\n ...opts,\n signal: controller.signal,\n headers: { \"User-Agent\": USER_AGENT, ...((opts.headers as Record<string, string>) || {}) },\n });\n } finally {\n clearTimeout(timer);\n }\n}\n\nexport async function getJson<T = unknown>(\n url: string,\n timeoutMs = DEFAULT_TIMEOUT_MS\n): Promise<T | null> {\n try {\n const res = await timedFetch(url, {}, timeoutMs);\n if (!res.ok) return null;\n return (await res.json()) as T;\n } catch {\n return null;\n }\n}\n\n/**\n * Tidy a raw label section: join arrays, strip a leading numbered ALL-CAPS\n * heading, collapse whitespace, and truncate on a sentence boundary.\n */\nexport function cleanSection(value: unknown, maxChars = MAX_FIELD_CHARS): string {\n if (!value) return \"\";\n let text = Array.isArray(value) ? value.join(\"\\n\\n\") : String(value);\n\n text = text\n .replace(/\\r/g, \"\")\n .replace(/[ \\t]+/g, \" \")\n .replace(/\\n{3,}/g, \"\\n\\n\")\n .trim();\n\n // Drop a leading section header line like \"5 WARNINGS AND PRECAUTIONS\"\n text = text.replace(/^\\s*\\d+(\\.\\d+)*\\s+[A-Z][A-Z \\-/,&]+\\n+/, \"\").trim();\n\n if (text.length > maxChars) {\n const slice = text.slice(0, maxChars);\n const lastStop = Math.max(slice.lastIndexOf(\". \"), slice.lastIndexOf(\"\\n\"));\n text = (lastStop > maxChars * 0.6 ? slice.slice(0, lastStop + 1) : slice).trim() + \" …\";\n }\n return text;\n}\n\nexport function formatFdaDate(yyyymmdd?: string): string {\n if (!yyyymmdd || !/^\\d{8}$/.test(yyyymmdd)) return \"\";\n const d = new Date(`${yyyymmdd.slice(0, 4)}-${yyyymmdd.slice(4, 6)}-${yyyymmdd.slice(6, 8)}`);\n if (isNaN(d.getTime())) return \"\";\n return d.toLocaleDateString(\"en-US\", { year: \"numeric\", month: \"short\", day: \"numeric\" });\n}\n\nexport function titleCase(s: string): string {\n return String(s || \"\").replace(/\\w\\S*/g, (t) => t.charAt(0).toUpperCase() + t.slice(1).toLowerCase());\n}\n\nexport function uniq<T>(arr: T[]): T[] {\n return [...new Set(arr.filter(Boolean))];\n}\n","/**\n * Deterministic brand <-> generic resolution via RxNorm/RxNav, with an openFDA\n * fallback. No AI, no hallucinated names. Brand names are US-registered;\n * international brand names are intentionally not invented.\n */\n\nimport { getJson, titleCase, uniq } from \"./fetch.js\";\nimport type { ResolveResult } from \"./types.js\";\n\nconst RXNAV = \"https://rxnav.nlm.nih.gov/REST\";\nconst OPENFDA = \"https://api.fda.gov/drug/label.json\";\nconst TIMEOUT_MS = 10000;\n\nconst REGION_NOTE = \"Brand names shown are those registered in the United States.\";\n\n/* ----------------------- RxNorm resolution ----------------------- */\n\nasync function findRxcui(name: string): Promise<string | null> {\n const exact = await getJson<any>(`${RXNAV}/rxcui.json?name=${encodeURIComponent(name)}`, TIMEOUT_MS);\n const id = exact?.idGroup?.rxnormId?.[0];\n if (id) return id;\n\n const approx = await getJson<any>(\n `${RXNAV}/approximateTerm.json?term=${encodeURIComponent(name)}&maxEntries=1`,\n TIMEOUT_MS\n );\n const cand = approx?.approximateGroup?.candidate?.[0];\n return cand ? cand.rxcui : null;\n}\n\nfunction collectNames(allRelated: any, tty: string): string[] {\n const groups = allRelated?.allRelatedGroup?.conceptGroup ?? [];\n const g = groups.find((x: any) => x.tty === tty);\n if (!g || !g.conceptProperties) return [];\n return g.conceptProperties.map((c: any) => c.name);\n}\n\n/**\n * Single-ingredient brand names for a generic ingredient, via SBDF concepts\n * whose name is \"<ingredient> <dose form> [Brand]\". Combination products read\n * \"<ing A> / <ing B> ... [Brand]\", so any \"/\" before the bracket is excluded.\n */\nasync function getSingleIngredientBrands(ingredientRxcui: string): Promise<string[]> {\n const data = await getJson<any>(`${RXNAV}/rxcui/${ingredientRxcui}/related.json?tty=SBDF`, TIMEOUT_MS);\n const groups = data?.relatedGroup?.conceptGroup ?? [];\n const g = groups.find((x: any) => x.tty === \"SBDF\");\n if (!g || !g.conceptProperties) return [];\n const brands: string[] = [];\n for (const c of g.conceptProperties) {\n const m = (c.name as string).match(/\\[([^\\]]+)\\]\\s*$/);\n if (!m) continue;\n const prefix = (c.name as string).slice(0, (c.name as string).lastIndexOf(\"[\"));\n if (prefix.includes(\"/\")) continue; // combination product — skip\n brands.push(m[1].trim());\n }\n return uniq(brands).sort();\n}\n\nasync function getDrugClassAndUses(\n rxcui: string\n): Promise<{ drugClass: string | null; uses: string[] }> {\n const data = await getJson<any>(\n `${RXNAV}/rxclass/class/byRxcui.json?rxcui=${rxcui}&relaSource=MEDRT`,\n TIMEOUT_MS\n );\n const items = data?.rxclassDrugInfoList?.rxclassDrugInfo ?? [];\n\n const byType = (t: string): string | null => {\n const hit = items.find((i: any) => i.rxclassMinConceptItem.classType === t);\n return hit ? hit.rxclassMinConceptItem.className : null;\n };\n // Prefer FDA Established Pharmacologic Class, then mechanism, then chemical.\n const drugClass = byType(\"EPC\") || byType(\"MOA\") || byType(\"CHEM\");\n\n const uses = uniq<string>(\n items\n .filter((i: any) => i.rela === \"may_treat\")\n .map((i: any) => i.rxclassMinConceptItem.className as string)\n ).slice(0, 6);\n\n return { drugClass, uses };\n}\n\nasync function resolveViaRxNorm(name: string): Promise<ResolveResult | null> {\n const rxcui = await findRxcui(name);\n if (!rxcui) return null;\n\n const [props, allRelated] = await Promise.all([\n getJson<any>(`${RXNAV}/rxcui/${rxcui}/properties.json`, TIMEOUT_MS),\n getJson<any>(`${RXNAV}/rxcui/${rxcui}/allrelated.json`, TIMEOUT_MS),\n ]);\n\n const tty: string | undefined = props?.properties?.tty;\n const canonicalName: string = props?.properties?.name ?? name;\n if (!tty) return null;\n\n const inNames = collectNames(allRelated, \"IN\");\n const minNames = collectNames(allRelated, \"MIN\");\n\n const isBrand = tty === \"BN\" || tty === \"SBD\" || tty === \"BPCK\";\n const inputType: \"brand\" | \"generic\" = isBrand ? \"brand\" : \"generic\";\n\n // For a generic, list only brands where it is the sole active ingredient.\n let brandNames: string[] | null = null;\n if (!isBrand) {\n const single = await getSingleIngredientBrands(rxcui);\n const fallback = uniq([...collectNames(allRelated, \"BN\"), ...collectNames(allRelated, \"BPCK\")]).sort();\n brandNames = single.length ? single : fallback;\n }\n\n // Generic display name: prefer the canonical combo string, else single, else joined.\n let genericName: string | null = null;\n if (minNames.length) genericName = titleCase(minNames[0]);\n else if (inNames.length === 1) genericName = titleCase(inNames[0]);\n else if (inNames.length > 1) genericName = titleCase(uniq(inNames).join(\" / \"));\n\n // Class + indications from the ingredient rxcui where possible.\n const classRxcui = !isBrand\n ? rxcui\n : (() => {\n const inGroup = (allRelated?.allRelatedGroup?.conceptGroup ?? []).find((x: any) => x.tty === \"IN\");\n return inGroup?.conceptProperties?.[0]?.rxcui ?? rxcui;\n })();\n const { drugClass, uses } = await getDrugClassAndUses(classRxcui);\n\n return {\n inputType,\n inputName: titleCase(canonicalName),\n genericName,\n brandNames,\n drugClass,\n uses,\n rxcui,\n source: \"RxNorm (NLM)\",\n regionNote: REGION_NOTE,\n };\n}\n\n/* ----------------------- openFDA fallback ------------------------ */\n\nasync function resolveViaOpenFda(name: string): Promise<ResolveResult | null> {\n const q = name.replace(/\"/g, \"\");\n const search = `(openfda.brand_name:\"${q}\"+OR+openfda.generic_name:\"${q}\")`;\n const url = `${OPENFDA}?search=${encodeURIComponent(search).replace(/%2B/g, \"+\")}&limit=1`;\n const data = await getJson<any>(url, TIMEOUT_MS);\n const fda = data?.results?.[0]?.openfda;\n if (!fda) return null;\n\n const brand = (fda.brand_name ?? [])[0] ?? \"\";\n const generic = (fda.generic_name ?? [])[0] ?? \"\";\n if (!brand && !generic) return null;\n\n const isBrand = brand.toLowerCase() === name.toLowerCase();\n return {\n inputType: isBrand ? \"brand\" : \"generic\",\n inputName: titleCase(name),\n genericName: generic ? titleCase(generic) : null,\n brandNames: isBrand ? null : uniq<string>(fda.brand_name ?? []).sort(),\n drugClass: (fda.pharm_class_epc ?? [])[0] ?? (fda.pharm_class_moa ?? [])[0] ?? null,\n uses: [],\n rxcui: null,\n source: \"openFDA\",\n regionNote: REGION_NOTE,\n };\n}\n\n/* --------------------------- public ----------------------------- */\n\n/**\n * Resolve a drug name deterministically. Tries RxNorm, then openFDA. Returns\n * null if neither source recognises the name.\n */\nexport async function resolveName(name: string): Promise<ResolveResult | null> {\n if (!name || !name.trim()) return null;\n const clean = name.trim();\n const viaRx = await resolveViaRxNorm(clean).catch(() => null);\n if (viaRx && (viaRx.genericName || (viaRx.brandNames && viaRx.brandNames.length))) {\n return viaRx;\n }\n return await resolveViaOpenFda(clean).catch(() => null);\n}\n","/**\n * Official drug-label lookups.\n * - US: FDA label via the openFDA API -> DailyMed citation\n * - UK/EU: SmPC via the eMC (medicines.org.uk) -> eMC citation\n *\n * Every network call is best-effort: any failure returns null so callers never\n * break. Pure fetch + parse — caching belongs to the consumer.\n */\n\nimport * as cheerio from \"cheerio\";\nimport { timedFetch, getJson, cleanSection, formatFdaDate } from \"./fetch.js\";\nimport type { RegionLabel, LabelResult } from \"./types.js\";\n\nconst OPENFDA_URL = \"https://api.fda.gov/drug/label.json\";\nconst EMC_BASE = \"https://www.medicines.org.uk\";\n\ninterface OpenFdaLabel {\n effective_time?: string;\n dosage_and_administration?: string[];\n boxed_warning?: string[];\n warnings_and_cautions?: string[];\n warnings?: string[];\n adverse_reactions?: string[];\n indications_and_usage?: string[];\n contraindications?: string[];\n openfda?: {\n generic_name?: string[];\n brand_name?: string[];\n spl_set_id?: string[];\n };\n}\ninterface OpenFdaResponse {\n results?: OpenFdaLabel[];\n}\n\n/* --------------------------- US: openFDA --------------------------- */\n\nexport async function getUspi(drug: string): Promise<RegionLabel | null> {\n if (!drug) return null;\n const q = drug.trim().replace(/\"/g, \"\");\n const search = `(openfda.generic_name:\"${q}\"+OR+openfda.brand_name:\"${q}\")`;\n const url = `${OPENFDA_URL}?search=${encodeURIComponent(search).replace(/%2B/g, \"+\")}&sort=effective_time:desc&limit=5`;\n\n const data = await getJson<OpenFdaResponse>(url);\n const results = data?.results ?? [];\n if (results.length === 0) return null;\n\n // Prefer a single-ingredient product over combination labels.\n const ranked = results.slice().sort((a, b) => score(b, q) - score(a, q));\n const r = ranked[0];\n\n const dosage = cleanSection(r.dosage_and_administration);\n const boxed = cleanSection(r.boxed_warning);\n const warn = cleanSection(r.warnings_and_cautions ?? r.warnings);\n const warnings = [boxed ? `BOXED WARNING — ${boxed}` : \"\", warn].filter(Boolean).join(\"\\n\\n\");\n const adverse = cleanSection(r.adverse_reactions);\n const indications = cleanSection(r.indications_and_usage);\n const contraindications = cleanSection(r.contraindications);\n\n if (!dosage && !warnings && !adverse && !indications && !contraindications) return null;\n\n const setId = r.openfda?.spl_set_id?.[0] ?? \"\";\n const brand = r.openfda?.brand_name?.[0] ?? \"\";\n const generic = r.openfda?.generic_name?.[0] ?? q;\n\n return {\n region: \"US\",\n authority: \"FDA Prescribing Information\",\n productName: brand ? `${brand} (${generic})` : generic,\n indications: indications || null,\n dosage: dosage || null,\n contraindications: contraindications || null,\n warnings: warnings || null,\n adverse: adverse || null,\n updated: formatFdaDate(r.effective_time),\n sourceUrl: setId\n ? `https://dailymed.nlm.nih.gov/dailymed/drugInfo.cfm?setid=${setId}`\n : \"https://dailymed.nlm.nih.gov/dailymed/\",\n sourceLabel: \"DailyMed (U.S. National Library of Medicine)\",\n };\n}\n\nfunction score(r: OpenFdaLabel, q: string): number {\n const g = (r.openfda?.generic_name ?? []).join(\", \").toLowerCase();\n let s = 0;\n if (g && g.split(/,| and /).length === 1) s += 2; // single ingredient\n if (g.includes(q.toLowerCase())) s += 1;\n return s;\n}\n\n/* --------------------------- UK/EU: eMC ---------------------------- */\n\ninterface EmcMatch {\n product_id: string;\n name: string;\n}\n\nasync function searchEmc(drug: string): Promise<EmcMatch[]> {\n const url = `${EMC_BASE}/emc/search?${new URLSearchParams({ q: drug })}`;\n const res = await timedFetch(url);\n if (!res.ok) return [];\n const html = await res.text();\n const $ = cheerio.load(html);\n const out: EmcMatch[] = [];\n $(\"a[href*='/emc/product/']\").each((_, el) => {\n const href = $(el).attr(\"href\") || \"\";\n const m = href.match(/\\/emc\\/product\\/(\\d+)\\/smpc/);\n if (!m) return;\n const name = $(el).text().trim();\n if (!name || name.toLowerCase().includes(\"health professional\")) return;\n if (out.some((x) => x.product_id === m[1])) return;\n out.push({ product_id: m[1], name });\n });\n return out;\n}\n\n// SmPC section number -> the field we store it under\nconst SMPC_WANT: Record<string, keyof Pick<RegionLabel, \"indications\" | \"dosage\" | \"contraindications\" | \"warnings\" | \"adverse\">> = {\n \"4.1\": \"indications\",\n \"4.2\": \"dosage\",\n \"4.3\": \"contraindications\",\n \"4.4\": \"warnings\",\n \"4.8\": \"adverse\",\n};\n\n// Canonical SmPC section titles — used as a fallback heading detector\nconst SMPC_SECTION_NAMES: Record<string, string> = {\n \"4.1\": \"Therapeutic indications\",\n \"4.2\": \"Posology and method of administration\",\n \"4.3\": \"Contraindications\",\n \"4.4\": \"Special warnings and precautions for use\",\n \"4.5\": \"Interaction with other medicinal products\",\n \"4.6\": \"Fertility, pregnancy and lactation\",\n \"4.7\": \"Effects on ability to drive and use machines\",\n \"4.8\": \"Undesirable effects\",\n \"4.9\": \"Overdose\",\n \"5.1\": \"Pharmacodynamic properties\",\n \"5.2\": \"Pharmacokinetic properties\",\n \"5.3\": \"Preclinical safety data\",\n};\n\nfunction htmlToText(html: string): string {\n const $ = cheerio.load(html);\n $(\"br\").replaceWith(\"\\n\");\n $(\"li\").each((_, el) => {\n $(el).prepend(\"- \");\n $(el).append(\"\\n\");\n });\n $(\"p, div, tr, h1, h2, h3, h4, h5, h6\").each((_, el) => {\n $(el).append(\"\\n\");\n });\n return $.text().replace(/\\n{3,}/g, \"\\n\\n\").trim();\n}\n\n/**\n * Parse the wanted SmPC sections from eMC HTML. Detect numbered headings, then\n * collect sibling content up to the next heading, with a parent-text fallback.\n */\nfunction parseSmpcSections(html: string): Record<string, string> {\n const $ = cheerio.load(html);\n const sectionElements: Array<{ code: string; title: string; element: any }> = [];\n\n // Strategy 1: headings whose text starts with a section number\n $(\"[id*='SECTION'], [id*='section'], .sectionHeading, h2, h3, h4\").each((_, el) => {\n const text = $(el).text().trim();\n const match = text.match(/^(\\d+\\.?\\d*)\\s+(.+)/);\n if (match) sectionElements.push({ code: match[1], title: match[2].trim(), element: $(el) });\n });\n\n // Strategy 2: fall back to scanning leaf nodes for known section headers\n if (sectionElements.length === 0) {\n $(\"*\").each((_, el) => {\n const $el = $(el);\n if ($el.children().length > 0 && !$el.is(\"a, span, strong, em, b, i\")) return;\n const text = $el.text().trim();\n const match = text.match(/^(\\d+\\.?\\d*)\\s+(.+)/);\n if (match && SMPC_SECTION_NAMES[match[1]]) {\n sectionElements.push({ code: match[1], title: match[2].trim(), element: $el });\n }\n });\n }\n\n const fields: Record<string, string> = {};\n for (let i = 0; i < sectionElements.length; i++) {\n const current = sectionElements[i];\n const field = SMPC_WANT[current.code];\n if (!field) continue;\n const next = sectionElements[i + 1];\n\n let content = \"\";\n let node = current.element.next();\n while (node.length > 0) {\n if (next && node.is(next.element)) break;\n const nodeHtml = $.html(node);\n if (nodeHtml) content += htmlToText(nodeHtml) + \"\\n\";\n node = node.next();\n }\n\n if (!content.trim()) {\n const parentHtml = $.html(current.element.parent());\n if (parentHtml) {\n content = htmlToText(parentHtml).replace(current.element.text().trim(), \"\").trim();\n }\n }\n\n fields[field] = cleanSection(content);\n }\n return fields;\n}\n\nexport async function getSmpc(drug: string): Promise<RegionLabel | null> {\n if (!drug) return null;\n try {\n const matches = await searchEmc(drug);\n if (matches.length === 0) return null;\n\n const q = drug.toLowerCase();\n matches.sort(\n (a, b) =>\n (b.name.toLowerCase().includes(q) ? 1 : 0) - (a.name.toLowerCase().includes(q) ? 1 : 0)\n );\n const best = matches[0];\n\n const res = await timedFetch(`${EMC_BASE}/emc/product/${best.product_id}/smpc`);\n if (!res.ok) return null;\n const html = await res.text();\n const sec = parseSmpcSections(html);\n\n if (!sec.dosage && !sec.warnings && !sec.adverse && !sec.indications && !sec.contraindications)\n return null;\n\n return {\n region: \"UK/EU\",\n authority: \"Summary of Product Characteristics (SmPC)\",\n productName: best.name,\n indications: sec.indications || null,\n dosage: sec.dosage || null,\n contraindications: sec.contraindications || null,\n warnings: sec.warnings || null,\n adverse: sec.adverse || null,\n updated: \"\",\n sourceUrl: `${EMC_BASE}/emc/product/${best.product_id}/smpc`,\n sourceLabel: \"electronic medicines compendium (emc), UK\",\n };\n } catch {\n return null;\n }\n}\n\n/* --------------------------- public API ---------------------------- */\n\n/** Look up both labels for a drug. Each side is null if unavailable. Never throws. */\nexport async function getLabels(drug: string): Promise<LabelResult> {\n const [us, uk] = await Promise.all([\n getUspi(drug).catch(() => null),\n getSmpc(drug).catch(() => null),\n ]);\n return { us, uk };\n}\n","/**\n * US (FDA) vs UK/EU (SmPC) side-by-side label comparison across mapped topics.\n */\n\nimport { getLabels } from \"./labels.js\";\nimport type { CompareResult, LabelComparison } from \"./types.js\";\n\ntype LabelField = \"indications\" | \"dosage\" | \"contraindications\" | \"warnings\" | \"adverse\";\n\n/** Topics compared across US and UK labels, mapped to the fields that hold them. */\nexport const SECTION_MAP: Array<{ topic: string; field: LabelField }> = [\n { topic: \"Indications\", field: \"indications\" },\n { topic: \"Dosing\", field: \"dosage\" },\n { topic: \"Contraindications\", field: \"contraindications\" },\n { topic: \"Warnings\", field: \"warnings\" },\n { topic: \"Adverse Reactions\", field: \"adverse\" },\n];\n\nexport async function compareLabels(drug: string): Promise<CompareResult> {\n const { us, uk } = await getLabels(drug);\n\n const comparisons: LabelComparison[] = SECTION_MAP.map(({ topic, field }) => ({\n topic,\n us: us ? us[field] : null,\n uk: uk ? uk[field] : null,\n })).filter((c) => c.us || c.uk);\n\n return {\n drug,\n comparisons,\n usSource: us?.sourceUrl ?? null,\n ukSource: uk?.sourceUrl ?? null,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACKA,IAAM,qBAAqB;AAC3B,IAAM,kBAAkB;AAEjB,IAAM,aAAa;AAE1B,eAAsB,WACpB,KACA,OAAoB,CAAC,GACrB,YAAY,oBACO;AACnB,QAAM,aAAa,IAAI,gBAAgB;AACvC,QAAM,QAAQ,WAAW,MAAM,WAAW,MAAM,GAAG,SAAS;AAC5D,MAAI;AACF,WAAO,MAAM,MAAM,KAAK;AAAA,MACtB,GAAG;AAAA,MACH,QAAQ,WAAW;AAAA,MACnB,SAAS,EAAE,cAAc,YAAY,GAAK,KAAK,WAAsC,CAAC,EAAG;AAAA,IAC3F,CAAC;AAAA,EACH,UAAE;AACA,iBAAa,KAAK;AAAA,EACpB;AACF;AAEA,eAAsB,QACpB,KACA,YAAY,oBACO;AACnB,MAAI;AACF,UAAM,MAAM,MAAM,WAAW,KAAK,CAAC,GAAG,SAAS;AAC/C,QAAI,CAAC,IAAI,GAAI,QAAO;AACpB,WAAQ,MAAM,IAAI,KAAK;AAAA,EACzB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAMO,SAAS,aAAa,OAAgB,WAAW,iBAAyB;AAC/E,MAAI,CAAC,MAAO,QAAO;AACnB,MAAI,OAAO,MAAM,QAAQ,KAAK,IAAI,MAAM,KAAK,MAAM,IAAI,OAAO,KAAK;AAEnE,SAAO,KACJ,QAAQ,OAAO,EAAE,EACjB,QAAQ,WAAW,GAAG,EACtB,QAAQ,WAAW,MAAM,EACzB,KAAK;AAGR,SAAO,KAAK,QAAQ,0CAA0C,EAAE,EAAE,KAAK;AAEvE,MAAI,KAAK,SAAS,UAAU;AAC1B,UAAM,QAAQ,KAAK,MAAM,GAAG,QAAQ;AACpC,UAAM,WAAW,KAAK,IAAI,MAAM,YAAY,IAAI,GAAG,MAAM,YAAY,IAAI,CAAC;AAC1E,YAAQ,WAAW,WAAW,MAAM,MAAM,MAAM,GAAG,WAAW,CAAC,IAAI,OAAO,KAAK,IAAI;AAAA,EACrF;AACA,SAAO;AACT;AAEO,SAAS,cAAc,UAA2B;AACvD,MAAI,CAAC,YAAY,CAAC,UAAU,KAAK,QAAQ,EAAG,QAAO;AACnD,QAAM,IAAI,oBAAI,KAAK,GAAG,SAAS,MAAM,GAAG,CAAC,CAAC,IAAI,SAAS,MAAM,GAAG,CAAC,CAAC,IAAI,SAAS,MAAM,GAAG,CAAC,CAAC,EAAE;AAC5F,MAAI,MAAM,EAAE,QAAQ,CAAC,EAAG,QAAO;AAC/B,SAAO,EAAE,mBAAmB,SAAS,EAAE,MAAM,WAAW,OAAO,SAAS,KAAK,UAAU,CAAC;AAC1F;AAEO,SAAS,UAAU,GAAmB;AAC3C,SAAO,OAAO,KAAK,EAAE,EAAE,QAAQ,UAAU,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,IAAI,EAAE,MAAM,CAAC,EAAE,YAAY,CAAC;AACtG;AAEO,SAAS,KAAQ,KAAe;AACrC,SAAO,CAAC,GAAG,IAAI,IAAI,IAAI,OAAO,OAAO,CAAC,CAAC;AACzC;;;ACtEA,IAAM,QAAQ;AACd,IAAM,UAAU;AAChB,IAAM,aAAa;AAEnB,IAAM,cAAc;AAIpB,eAAe,UAAU,MAAsC;AAC7D,QAAM,QAAQ,MAAM,QAAa,GAAG,KAAK,oBAAoB,mBAAmB,IAAI,CAAC,IAAI,UAAU;AACnG,QAAM,KAAK,OAAO,SAAS,WAAW,CAAC;AACvC,MAAI,GAAI,QAAO;AAEf,QAAM,SAAS,MAAM;AAAA,IACnB,GAAG,KAAK,8BAA8B,mBAAmB,IAAI,CAAC;AAAA,IAC9D;AAAA,EACF;AACA,QAAM,OAAO,QAAQ,kBAAkB,YAAY,CAAC;AACpD,SAAO,OAAO,KAAK,QAAQ;AAC7B;AAEA,SAAS,aAAa,YAAiB,KAAuB;AAC5D,QAAM,SAAS,YAAY,iBAAiB,gBAAgB,CAAC;AAC7D,QAAM,IAAI,OAAO,KAAK,CAAC,MAAW,EAAE,QAAQ,GAAG;AAC/C,MAAI,CAAC,KAAK,CAAC,EAAE,kBAAmB,QAAO,CAAC;AACxC,SAAO,EAAE,kBAAkB,IAAI,CAAC,MAAW,EAAE,IAAI;AACnD;AAOA,eAAe,0BAA0B,iBAA4C;AACnF,QAAM,OAAO,MAAM,QAAa,GAAG,KAAK,UAAU,eAAe,0BAA0B,UAAU;AACrG,QAAM,SAAS,MAAM,cAAc,gBAAgB,CAAC;AACpD,QAAM,IAAI,OAAO,KAAK,CAAC,MAAW,EAAE,QAAQ,MAAM;AAClD,MAAI,CAAC,KAAK,CAAC,EAAE,kBAAmB,QAAO,CAAC;AACxC,QAAM,SAAmB,CAAC;AAC1B,aAAW,KAAK,EAAE,mBAAmB;AACnC,UAAM,IAAK,EAAE,KAAgB,MAAM,kBAAkB;AACrD,QAAI,CAAC,EAAG;AACR,UAAM,SAAU,EAAE,KAAgB,MAAM,GAAI,EAAE,KAAgB,YAAY,GAAG,CAAC;AAC9E,QAAI,OAAO,SAAS,GAAG,EAAG;AAC1B,WAAO,KAAK,EAAE,CAAC,EAAE,KAAK,CAAC;AAAA,EACzB;AACA,SAAO,KAAK,MAAM,EAAE,KAAK;AAC3B;AAEA,eAAe,oBACb,OACuD;AACvD,QAAM,OAAO,MAAM;AAAA,IACjB,GAAG,KAAK,qCAAqC,KAAK;AAAA,IAClD;AAAA,EACF;AACA,QAAM,QAAQ,MAAM,qBAAqB,mBAAmB,CAAC;AAE7D,QAAM,SAAS,CAAC,MAA6B;AAC3C,UAAM,MAAM,MAAM,KAAK,CAAC,MAAW,EAAE,sBAAsB,cAAc,CAAC;AAC1E,WAAO,MAAM,IAAI,sBAAsB,YAAY;AAAA,EACrD;AAEA,QAAM,YAAY,OAAO,KAAK,KAAK,OAAO,KAAK,KAAK,OAAO,MAAM;AAEjE,QAAM,OAAO;AAAA,IACX,MACG,OAAO,CAAC,MAAW,EAAE,SAAS,WAAW,EACzC,IAAI,CAAC,MAAW,EAAE,sBAAsB,SAAmB;AAAA,EAChE,EAAE,MAAM,GAAG,CAAC;AAEZ,SAAO,EAAE,WAAW,KAAK;AAC3B;AAEA,eAAe,iBAAiB,MAA6C;AAC3E,QAAM,QAAQ,MAAM,UAAU,IAAI;AAClC,MAAI,CAAC,MAAO,QAAO;AAEnB,QAAM,CAAC,OAAO,UAAU,IAAI,MAAM,QAAQ,IAAI;AAAA,IAC5C,QAAa,GAAG,KAAK,UAAU,KAAK,oBAAoB,UAAU;AAAA,IAClE,QAAa,GAAG,KAAK,UAAU,KAAK,oBAAoB,UAAU;AAAA,EACpE,CAAC;AAED,QAAM,MAA0B,OAAO,YAAY;AACnD,QAAM,gBAAwB,OAAO,YAAY,QAAQ;AACzD,MAAI,CAAC,IAAK,QAAO;AAEjB,QAAM,UAAU,aAAa,YAAY,IAAI;AAC7C,QAAM,WAAW,aAAa,YAAY,KAAK;AAE/C,QAAM,UAAU,QAAQ,QAAQ,QAAQ,SAAS,QAAQ;AACzD,QAAM,YAAiC,UAAU,UAAU;AAG3D,MAAI,aAA8B;AAClC,MAAI,CAAC,SAAS;AACZ,UAAM,SAAS,MAAM,0BAA0B,KAAK;AACpD,UAAM,WAAW,KAAK,CAAC,GAAG,aAAa,YAAY,IAAI,GAAG,GAAG,aAAa,YAAY,MAAM,CAAC,CAAC,EAAE,KAAK;AACrG,iBAAa,OAAO,SAAS,SAAS;AAAA,EACxC;AAGA,MAAI,cAA6B;AACjC,MAAI,SAAS,OAAQ,eAAc,UAAU,SAAS,CAAC,CAAC;AAAA,WAC/C,QAAQ,WAAW,EAAG,eAAc,UAAU,QAAQ,CAAC,CAAC;AAAA,WACxD,QAAQ,SAAS,EAAG,eAAc,UAAU,KAAK,OAAO,EAAE,KAAK,KAAK,CAAC;AAG9E,QAAM,aAAa,CAAC,UAChB,SACC,MAAM;AACL,UAAM,WAAW,YAAY,iBAAiB,gBAAgB,CAAC,GAAG,KAAK,CAAC,MAAW,EAAE,QAAQ,IAAI;AACjG,WAAO,SAAS,oBAAoB,CAAC,GAAG,SAAS;AAAA,EACnD,GAAG;AACP,QAAM,EAAE,WAAW,KAAK,IAAI,MAAM,oBAAoB,UAAU;AAEhE,SAAO;AAAA,IACL;AAAA,IACA,WAAW,UAAU,aAAa;AAAA,IAClC;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,QAAQ;AAAA,IACR,YAAY;AAAA,EACd;AACF;AAIA,eAAe,kBAAkB,MAA6C;AAC5E,QAAM,IAAI,KAAK,QAAQ,MAAM,EAAE;AAC/B,QAAM,SAAS,wBAAwB,CAAC,8BAA8B,CAAC;AACvE,QAAM,MAAM,GAAG,OAAO,WAAW,mBAAmB,MAAM,EAAE,QAAQ,QAAQ,GAAG,CAAC;AAChF,QAAM,OAAO,MAAM,QAAa,KAAK,UAAU;AAC/C,QAAM,MAAM,MAAM,UAAU,CAAC,GAAG;AAChC,MAAI,CAAC,IAAK,QAAO;AAEjB,QAAM,SAAS,IAAI,cAAc,CAAC,GAAG,CAAC,KAAK;AAC3C,QAAM,WAAW,IAAI,gBAAgB,CAAC,GAAG,CAAC,KAAK;AAC/C,MAAI,CAAC,SAAS,CAAC,QAAS,QAAO;AAE/B,QAAM,UAAU,MAAM,YAAY,MAAM,KAAK,YAAY;AACzD,SAAO;AAAA,IACL,WAAW,UAAU,UAAU;AAAA,IAC/B,WAAW,UAAU,IAAI;AAAA,IACzB,aAAa,UAAU,UAAU,OAAO,IAAI;AAAA,IAC5C,YAAY,UAAU,OAAO,KAAa,IAAI,cAAc,CAAC,CAAC,EAAE,KAAK;AAAA,IACrE,YAAY,IAAI,mBAAmB,CAAC,GAAG,CAAC,MAAM,IAAI,mBAAmB,CAAC,GAAG,CAAC,KAAK;AAAA,IAC/E,MAAM,CAAC;AAAA,IACP,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,YAAY;AAAA,EACd;AACF;AAQA,eAAsB,YAAY,MAA6C;AAC7E,MAAI,CAAC,QAAQ,CAAC,KAAK,KAAK,EAAG,QAAO;AAClC,QAAM,QAAQ,KAAK,KAAK;AACxB,QAAM,QAAQ,MAAM,iBAAiB,KAAK,EAAE,MAAM,MAAM,IAAI;AAC5D,MAAI,UAAU,MAAM,eAAgB,MAAM,cAAc,MAAM,WAAW,SAAU;AACjF,WAAO;AAAA,EACT;AACA,SAAO,MAAM,kBAAkB,KAAK,EAAE,MAAM,MAAM,IAAI;AACxD;;;AC3KA,cAAyB;AAIzB,IAAM,cAAc;AACpB,IAAM,WAAW;AAuBjB,eAAsB,QAAQ,MAA2C;AACvE,MAAI,CAAC,KAAM,QAAO;AAClB,QAAM,IAAI,KAAK,KAAK,EAAE,QAAQ,MAAM,EAAE;AACtC,QAAM,SAAS,0BAA0B,CAAC,4BAA4B,CAAC;AACvE,QAAM,MAAM,GAAG,WAAW,WAAW,mBAAmB,MAAM,EAAE,QAAQ,QAAQ,GAAG,CAAC;AAEpF,QAAM,OAAO,MAAM,QAAyB,GAAG;AAC/C,QAAM,UAAU,MAAM,WAAW,CAAC;AAClC,MAAI,QAAQ,WAAW,EAAG,QAAO;AAGjC,QAAM,SAAS,QAAQ,MAAM,EAAE,KAAK,CAAC,GAAG,MAAM,MAAM,GAAG,CAAC,IAAI,MAAM,GAAG,CAAC,CAAC;AACvE,QAAM,IAAI,OAAO,CAAC;AAElB,QAAM,SAAS,aAAa,EAAE,yBAAyB;AACvD,QAAM,QAAQ,aAAa,EAAE,aAAa;AAC1C,QAAM,OAAO,aAAa,EAAE,yBAAyB,EAAE,QAAQ;AAC/D,QAAM,WAAW,CAAC,QAAQ,wBAAmB,KAAK,KAAK,IAAI,IAAI,EAAE,OAAO,OAAO,EAAE,KAAK,MAAM;AAC5F,QAAM,UAAU,aAAa,EAAE,iBAAiB;AAChD,QAAM,cAAc,aAAa,EAAE,qBAAqB;AACxD,QAAM,oBAAoB,aAAa,EAAE,iBAAiB;AAE1D,MAAI,CAAC,UAAU,CAAC,YAAY,CAAC,WAAW,CAAC,eAAe,CAAC,kBAAmB,QAAO;AAEnF,QAAM,QAAQ,EAAE,SAAS,aAAa,CAAC,KAAK;AAC5C,QAAM,QAAQ,EAAE,SAAS,aAAa,CAAC,KAAK;AAC5C,QAAM,UAAU,EAAE,SAAS,eAAe,CAAC,KAAK;AAEhD,SAAO;AAAA,IACL,QAAQ;AAAA,IACR,WAAW;AAAA,IACX,aAAa,QAAQ,GAAG,KAAK,KAAK,OAAO,MAAM;AAAA,IAC/C,aAAa,eAAe;AAAA,IAC5B,QAAQ,UAAU;AAAA,IAClB,mBAAmB,qBAAqB;AAAA,IACxC,UAAU,YAAY;AAAA,IACtB,SAAS,WAAW;AAAA,IACpB,SAAS,cAAc,EAAE,cAAc;AAAA,IACvC,WAAW,QACP,4DAA4D,KAAK,KACjE;AAAA,IACJ,aAAa;AAAA,EACf;AACF;AAEA,SAAS,MAAM,GAAiB,GAAmB;AACjD,QAAM,KAAK,EAAE,SAAS,gBAAgB,CAAC,GAAG,KAAK,IAAI,EAAE,YAAY;AACjE,MAAI,IAAI;AACR,MAAI,KAAK,EAAE,MAAM,SAAS,EAAE,WAAW,EAAG,MAAK;AAC/C,MAAI,EAAE,SAAS,EAAE,YAAY,CAAC,EAAG,MAAK;AACtC,SAAO;AACT;AASA,eAAe,UAAU,MAAmC;AAC1D,QAAM,MAAM,GAAG,QAAQ,eAAe,IAAI,gBAAgB,EAAE,GAAG,KAAK,CAAC,CAAC;AACtE,QAAM,MAAM,MAAM,WAAW,GAAG;AAChC,MAAI,CAAC,IAAI,GAAI,QAAO,CAAC;AACrB,QAAM,OAAO,MAAM,IAAI,KAAK;AAC5B,QAAM,IAAY,aAAK,IAAI;AAC3B,QAAM,MAAkB,CAAC;AACzB,IAAE,0BAA0B,EAAE,KAAK,CAAC,GAAG,OAAO;AAC5C,UAAM,OAAO,EAAE,EAAE,EAAE,KAAK,MAAM,KAAK;AACnC,UAAM,IAAI,KAAK,MAAM,6BAA6B;AAClD,QAAI,CAAC,EAAG;AACR,UAAM,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK;AAC/B,QAAI,CAAC,QAAQ,KAAK,YAAY,EAAE,SAAS,qBAAqB,EAAG;AACjE,QAAI,IAAI,KAAK,CAAC,MAAM,EAAE,eAAe,EAAE,CAAC,CAAC,EAAG;AAC5C,QAAI,KAAK,EAAE,YAAY,EAAE,CAAC,GAAG,KAAK,CAAC;AAAA,EACrC,CAAC;AACD,SAAO;AACT;AAGA,IAAM,YAA8H;AAAA,EAClI,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AACT;AAGA,IAAM,qBAA6C;AAAA,EACjD,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AACT;AAEA,SAAS,WAAW,MAAsB;AACxC,QAAM,IAAY,aAAK,IAAI;AAC3B,IAAE,IAAI,EAAE,YAAY,IAAI;AACxB,IAAE,IAAI,EAAE,KAAK,CAAC,GAAG,OAAO;AACtB,MAAE,EAAE,EAAE,QAAQ,IAAI;AAClB,MAAE,EAAE,EAAE,OAAO,IAAI;AAAA,EACnB,CAAC;AACD,IAAE,oCAAoC,EAAE,KAAK,CAAC,GAAG,OAAO;AACtD,MAAE,EAAE,EAAE,OAAO,IAAI;AAAA,EACnB,CAAC;AACD,SAAO,EAAE,KAAK,EAAE,QAAQ,WAAW,MAAM,EAAE,KAAK;AAClD;AAMA,SAAS,kBAAkB,MAAsC;AAC/D,QAAM,IAAY,aAAK,IAAI;AAC3B,QAAM,kBAAwE,CAAC;AAG/E,IAAE,+DAA+D,EAAE,KAAK,CAAC,GAAG,OAAO;AACjF,UAAM,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK;AAC/B,UAAM,QAAQ,KAAK,MAAM,qBAAqB;AAC9C,QAAI,MAAO,iBAAgB,KAAK,EAAE,MAAM,MAAM,CAAC,GAAG,OAAO,MAAM,CAAC,EAAE,KAAK,GAAG,SAAS,EAAE,EAAE,EAAE,CAAC;AAAA,EAC5F,CAAC;AAGD,MAAI,gBAAgB,WAAW,GAAG;AAChC,MAAE,GAAG,EAAE,KAAK,CAAC,GAAG,OAAO;AACrB,YAAM,MAAM,EAAE,EAAE;AAChB,UAAI,IAAI,SAAS,EAAE,SAAS,KAAK,CAAC,IAAI,GAAG,2BAA2B,EAAG;AACvE,YAAM,OAAO,IAAI,KAAK,EAAE,KAAK;AAC7B,YAAM,QAAQ,KAAK,MAAM,qBAAqB;AAC9C,UAAI,SAAS,mBAAmB,MAAM,CAAC,CAAC,GAAG;AACzC,wBAAgB,KAAK,EAAE,MAAM,MAAM,CAAC,GAAG,OAAO,MAAM,CAAC,EAAE,KAAK,GAAG,SAAS,IAAI,CAAC;AAAA,MAC/E;AAAA,IACF,CAAC;AAAA,EACH;AAEA,QAAM,SAAiC,CAAC;AACxC,WAAS,IAAI,GAAG,IAAI,gBAAgB,QAAQ,KAAK;AAC/C,UAAM,UAAU,gBAAgB,CAAC;AACjC,UAAM,QAAQ,UAAU,QAAQ,IAAI;AACpC,QAAI,CAAC,MAAO;AACZ,UAAM,OAAO,gBAAgB,IAAI,CAAC;AAElC,QAAI,UAAU;AACd,QAAI,OAAO,QAAQ,QAAQ,KAAK;AAChC,WAAO,KAAK,SAAS,GAAG;AACtB,UAAI,QAAQ,KAAK,GAAG,KAAK,OAAO,EAAG;AACnC,YAAM,WAAW,EAAE,KAAK,IAAI;AAC5B,UAAI,SAAU,YAAW,WAAW,QAAQ,IAAI;AAChD,aAAO,KAAK,KAAK;AAAA,IACnB;AAEA,QAAI,CAAC,QAAQ,KAAK,GAAG;AACnB,YAAM,aAAa,EAAE,KAAK,QAAQ,QAAQ,OAAO,CAAC;AAClD,UAAI,YAAY;AACd,kBAAU,WAAW,UAAU,EAAE,QAAQ,QAAQ,QAAQ,KAAK,EAAE,KAAK,GAAG,EAAE,EAAE,KAAK;AAAA,MACnF;AAAA,IACF;AAEA,WAAO,KAAK,IAAI,aAAa,OAAO;AAAA,EACtC;AACA,SAAO;AACT;AAEA,eAAsB,QAAQ,MAA2C;AACvE,MAAI,CAAC,KAAM,QAAO;AAClB,MAAI;AACF,UAAM,UAAU,MAAM,UAAU,IAAI;AACpC,QAAI,QAAQ,WAAW,EAAG,QAAO;AAEjC,UAAM,IAAI,KAAK,YAAY;AAC3B,YAAQ;AAAA,MACN,CAAC,GAAG,OACD,EAAE,KAAK,YAAY,EAAE,SAAS,CAAC,IAAI,IAAI,MAAM,EAAE,KAAK,YAAY,EAAE,SAAS,CAAC,IAAI,IAAI;AAAA,IACzF;AACA,UAAM,OAAO,QAAQ,CAAC;AAEtB,UAAM,MAAM,MAAM,WAAW,GAAG,QAAQ,gBAAgB,KAAK,UAAU,OAAO;AAC9E,QAAI,CAAC,IAAI,GAAI,QAAO;AACpB,UAAM,OAAO,MAAM,IAAI,KAAK;AAC5B,UAAM,MAAM,kBAAkB,IAAI;AAElC,QAAI,CAAC,IAAI,UAAU,CAAC,IAAI,YAAY,CAAC,IAAI,WAAW,CAAC,IAAI,eAAe,CAAC,IAAI;AAC3E,aAAO;AAET,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,WAAW;AAAA,MACX,aAAa,KAAK;AAAA,MAClB,aAAa,IAAI,eAAe;AAAA,MAChC,QAAQ,IAAI,UAAU;AAAA,MACtB,mBAAmB,IAAI,qBAAqB;AAAA,MAC5C,UAAU,IAAI,YAAY;AAAA,MAC1B,SAAS,IAAI,WAAW;AAAA,MACxB,SAAS;AAAA,MACT,WAAW,GAAG,QAAQ,gBAAgB,KAAK,UAAU;AAAA,MACrD,aAAa;AAAA,IACf;AAAA,EACF,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAKA,eAAsB,UAAU,MAAoC;AAClE,QAAM,CAAC,IAAI,EAAE,IAAI,MAAM,QAAQ,IAAI;AAAA,IACjC,QAAQ,IAAI,EAAE,MAAM,MAAM,IAAI;AAAA,IAC9B,QAAQ,IAAI,EAAE,MAAM,MAAM,IAAI;AAAA,EAChC,CAAC;AACD,SAAO,EAAE,IAAI,GAAG;AAClB;;;ACxPO,IAAM,cAA2D;AAAA,EACtE,EAAE,OAAO,eAAe,OAAO,cAAc;AAAA,EAC7C,EAAE,OAAO,UAAU,OAAO,SAAS;AAAA,EACnC,EAAE,OAAO,qBAAqB,OAAO,oBAAoB;AAAA,EACzD,EAAE,OAAO,YAAY,OAAO,WAAW;AAAA,EACvC,EAAE,OAAO,qBAAqB,OAAO,UAAU;AACjD;AAEA,eAAsB,cAAc,MAAsC;AACxE,QAAM,EAAE,IAAI,GAAG,IAAI,MAAM,UAAU,IAAI;AAEvC,QAAM,cAAiC,YAAY,IAAI,CAAC,EAAE,OAAO,MAAM,OAAO;AAAA,IAC5E;AAAA,IACA,IAAI,KAAK,GAAG,KAAK,IAAI;AAAA,IACrB,IAAI,KAAK,GAAG,KAAK,IAAI;AAAA,EACvB,EAAE,EAAE,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE;AAE9B,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,UAAU,IAAI,aAAa;AAAA,IAC3B,UAAU,IAAI,aAAa;AAAA,EAC7B;AACF;","names":[]}
@@ -0,0 +1,92 @@
1
+ type Region = "US" | "UK/EU";
2
+ /** One regional label (FDA USPI or UK/EU SmPC), cleaned and truncated. */
3
+ interface RegionLabel {
4
+ region: Region;
5
+ authority: string;
6
+ productName: string;
7
+ indications: string | null;
8
+ dosage: string | null;
9
+ contraindications: string | null;
10
+ warnings: string | null;
11
+ adverse: string | null;
12
+ /** Human-readable revision date (US only; "" when unknown). */
13
+ updated: string;
14
+ sourceUrl: string;
15
+ sourceLabel: string;
16
+ }
17
+ interface LabelResult {
18
+ us: RegionLabel | null;
19
+ uk: RegionLabel | null;
20
+ }
21
+ /** Deterministic brand <-> generic resolution result. */
22
+ interface ResolveResult {
23
+ inputType: "brand" | "generic";
24
+ inputName: string;
25
+ genericName: string | null;
26
+ /** US brand names (null when the input itself is a brand). */
27
+ brandNames: string[] | null;
28
+ drugClass: string | null;
29
+ uses: string[];
30
+ rxcui: string | null;
31
+ source: string;
32
+ regionNote: string;
33
+ }
34
+ /** One topic compared across US and UK labels. */
35
+ interface LabelComparison {
36
+ topic: string;
37
+ us: string | null;
38
+ uk: string | null;
39
+ }
40
+ interface CompareResult {
41
+ drug: string;
42
+ comparisons: LabelComparison[];
43
+ usSource: string | null;
44
+ ukSource: string | null;
45
+ }
46
+
47
+ /**
48
+ * Deterministic brand <-> generic resolution via RxNorm/RxNav, with an openFDA
49
+ * fallback. No AI, no hallucinated names. Brand names are US-registered;
50
+ * international brand names are intentionally not invented.
51
+ */
52
+
53
+ /**
54
+ * Resolve a drug name deterministically. Tries RxNorm, then openFDA. Returns
55
+ * null if neither source recognises the name.
56
+ */
57
+ declare function resolveName(name: string): Promise<ResolveResult | null>;
58
+
59
+ /**
60
+ * Official drug-label lookups.
61
+ * - US: FDA label via the openFDA API -> DailyMed citation
62
+ * - UK/EU: SmPC via the eMC (medicines.org.uk) -> eMC citation
63
+ *
64
+ * Every network call is best-effort: any failure returns null so callers never
65
+ * break. Pure fetch + parse — caching belongs to the consumer.
66
+ */
67
+
68
+ declare function getUspi(drug: string): Promise<RegionLabel | null>;
69
+ declare function getSmpc(drug: string): Promise<RegionLabel | null>;
70
+ /** Look up both labels for a drug. Each side is null if unavailable. Never throws. */
71
+ declare function getLabels(drug: string): Promise<LabelResult>;
72
+
73
+ /**
74
+ * US (FDA) vs UK/EU (SmPC) side-by-side label comparison across mapped topics.
75
+ */
76
+
77
+ type LabelField = "indications" | "dosage" | "contraindications" | "warnings" | "adverse";
78
+ /** Topics compared across US and UK labels, mapped to the fields that hold them. */
79
+ declare const SECTION_MAP: Array<{
80
+ topic: string;
81
+ field: LabelField;
82
+ }>;
83
+ declare function compareLabels(drug: string): Promise<CompareResult>;
84
+
85
+ /**
86
+ * Tidy a raw label section: join arrays, strip a leading numbered ALL-CAPS
87
+ * heading, collapse whitespace, and truncate on a sentence boundary.
88
+ */
89
+ declare function cleanSection(value: unknown, maxChars?: number): string;
90
+ declare function formatFdaDate(yyyymmdd?: string): string;
91
+
92
+ export { type CompareResult, type LabelComparison, type LabelResult, type Region, type RegionLabel, type ResolveResult, SECTION_MAP, cleanSection, compareLabels, formatFdaDate, getLabels, getSmpc, getUspi, resolveName };
@@ -0,0 +1,92 @@
1
+ type Region = "US" | "UK/EU";
2
+ /** One regional label (FDA USPI or UK/EU SmPC), cleaned and truncated. */
3
+ interface RegionLabel {
4
+ region: Region;
5
+ authority: string;
6
+ productName: string;
7
+ indications: string | null;
8
+ dosage: string | null;
9
+ contraindications: string | null;
10
+ warnings: string | null;
11
+ adverse: string | null;
12
+ /** Human-readable revision date (US only; "" when unknown). */
13
+ updated: string;
14
+ sourceUrl: string;
15
+ sourceLabel: string;
16
+ }
17
+ interface LabelResult {
18
+ us: RegionLabel | null;
19
+ uk: RegionLabel | null;
20
+ }
21
+ /** Deterministic brand <-> generic resolution result. */
22
+ interface ResolveResult {
23
+ inputType: "brand" | "generic";
24
+ inputName: string;
25
+ genericName: string | null;
26
+ /** US brand names (null when the input itself is a brand). */
27
+ brandNames: string[] | null;
28
+ drugClass: string | null;
29
+ uses: string[];
30
+ rxcui: string | null;
31
+ source: string;
32
+ regionNote: string;
33
+ }
34
+ /** One topic compared across US and UK labels. */
35
+ interface LabelComparison {
36
+ topic: string;
37
+ us: string | null;
38
+ uk: string | null;
39
+ }
40
+ interface CompareResult {
41
+ drug: string;
42
+ comparisons: LabelComparison[];
43
+ usSource: string | null;
44
+ ukSource: string | null;
45
+ }
46
+
47
+ /**
48
+ * Deterministic brand <-> generic resolution via RxNorm/RxNav, with an openFDA
49
+ * fallback. No AI, no hallucinated names. Brand names are US-registered;
50
+ * international brand names are intentionally not invented.
51
+ */
52
+
53
+ /**
54
+ * Resolve a drug name deterministically. Tries RxNorm, then openFDA. Returns
55
+ * null if neither source recognises the name.
56
+ */
57
+ declare function resolveName(name: string): Promise<ResolveResult | null>;
58
+
59
+ /**
60
+ * Official drug-label lookups.
61
+ * - US: FDA label via the openFDA API -> DailyMed citation
62
+ * - UK/EU: SmPC via the eMC (medicines.org.uk) -> eMC citation
63
+ *
64
+ * Every network call is best-effort: any failure returns null so callers never
65
+ * break. Pure fetch + parse — caching belongs to the consumer.
66
+ */
67
+
68
+ declare function getUspi(drug: string): Promise<RegionLabel | null>;
69
+ declare function getSmpc(drug: string): Promise<RegionLabel | null>;
70
+ /** Look up both labels for a drug. Each side is null if unavailable. Never throws. */
71
+ declare function getLabels(drug: string): Promise<LabelResult>;
72
+
73
+ /**
74
+ * US (FDA) vs UK/EU (SmPC) side-by-side label comparison across mapped topics.
75
+ */
76
+
77
+ type LabelField = "indications" | "dosage" | "contraindications" | "warnings" | "adverse";
78
+ /** Topics compared across US and UK labels, mapped to the fields that hold them. */
79
+ declare const SECTION_MAP: Array<{
80
+ topic: string;
81
+ field: LabelField;
82
+ }>;
83
+ declare function compareLabels(drug: string): Promise<CompareResult>;
84
+
85
+ /**
86
+ * Tidy a raw label section: join arrays, strip a leading numbered ALL-CAPS
87
+ * heading, collapse whitespace, and truncate on a sentence boundary.
88
+ */
89
+ declare function cleanSection(value: unknown, maxChars?: number): string;
90
+ declare function formatFdaDate(yyyymmdd?: string): string;
91
+
92
+ export { type CompareResult, type LabelComparison, type LabelResult, type Region, type RegionLabel, type ResolveResult, SECTION_MAP, cleanSection, compareLabels, formatFdaDate, getLabels, getSmpc, getUspi, resolveName };
package/dist/index.js ADDED
@@ -0,0 +1,392 @@
1
+ // src/fetch.ts
2
+ var DEFAULT_TIMEOUT_MS = 12e3;
3
+ var MAX_FIELD_CHARS = 1400;
4
+ var USER_AGENT = "drug-data/0.1 (pharmatools.ai; nick@pharmatools.ai)";
5
+ async function timedFetch(url, opts = {}, timeoutMs = DEFAULT_TIMEOUT_MS) {
6
+ const controller = new AbortController();
7
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
8
+ try {
9
+ return await fetch(url, {
10
+ ...opts,
11
+ signal: controller.signal,
12
+ headers: { "User-Agent": USER_AGENT, ...opts.headers || {} }
13
+ });
14
+ } finally {
15
+ clearTimeout(timer);
16
+ }
17
+ }
18
+ async function getJson(url, timeoutMs = DEFAULT_TIMEOUT_MS) {
19
+ try {
20
+ const res = await timedFetch(url, {}, timeoutMs);
21
+ if (!res.ok) return null;
22
+ return await res.json();
23
+ } catch {
24
+ return null;
25
+ }
26
+ }
27
+ function cleanSection(value, maxChars = MAX_FIELD_CHARS) {
28
+ if (!value) return "";
29
+ let text = Array.isArray(value) ? value.join("\n\n") : String(value);
30
+ text = text.replace(/\r/g, "").replace(/[ \t]+/g, " ").replace(/\n{3,}/g, "\n\n").trim();
31
+ text = text.replace(/^\s*\d+(\.\d+)*\s+[A-Z][A-Z \-/,&]+\n+/, "").trim();
32
+ if (text.length > maxChars) {
33
+ const slice = text.slice(0, maxChars);
34
+ const lastStop = Math.max(slice.lastIndexOf(". "), slice.lastIndexOf("\n"));
35
+ text = (lastStop > maxChars * 0.6 ? slice.slice(0, lastStop + 1) : slice).trim() + " \u2026";
36
+ }
37
+ return text;
38
+ }
39
+ function formatFdaDate(yyyymmdd) {
40
+ if (!yyyymmdd || !/^\d{8}$/.test(yyyymmdd)) return "";
41
+ const d = /* @__PURE__ */ new Date(`${yyyymmdd.slice(0, 4)}-${yyyymmdd.slice(4, 6)}-${yyyymmdd.slice(6, 8)}`);
42
+ if (isNaN(d.getTime())) return "";
43
+ return d.toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric" });
44
+ }
45
+ function titleCase(s) {
46
+ return String(s || "").replace(/\w\S*/g, (t) => t.charAt(0).toUpperCase() + t.slice(1).toLowerCase());
47
+ }
48
+ function uniq(arr) {
49
+ return [...new Set(arr.filter(Boolean))];
50
+ }
51
+
52
+ // src/names.ts
53
+ var RXNAV = "https://rxnav.nlm.nih.gov/REST";
54
+ var OPENFDA = "https://api.fda.gov/drug/label.json";
55
+ var TIMEOUT_MS = 1e4;
56
+ var REGION_NOTE = "Brand names shown are those registered in the United States.";
57
+ async function findRxcui(name) {
58
+ const exact = await getJson(`${RXNAV}/rxcui.json?name=${encodeURIComponent(name)}`, TIMEOUT_MS);
59
+ const id = exact?.idGroup?.rxnormId?.[0];
60
+ if (id) return id;
61
+ const approx = await getJson(
62
+ `${RXNAV}/approximateTerm.json?term=${encodeURIComponent(name)}&maxEntries=1`,
63
+ TIMEOUT_MS
64
+ );
65
+ const cand = approx?.approximateGroup?.candidate?.[0];
66
+ return cand ? cand.rxcui : null;
67
+ }
68
+ function collectNames(allRelated, tty) {
69
+ const groups = allRelated?.allRelatedGroup?.conceptGroup ?? [];
70
+ const g = groups.find((x) => x.tty === tty);
71
+ if (!g || !g.conceptProperties) return [];
72
+ return g.conceptProperties.map((c) => c.name);
73
+ }
74
+ async function getSingleIngredientBrands(ingredientRxcui) {
75
+ const data = await getJson(`${RXNAV}/rxcui/${ingredientRxcui}/related.json?tty=SBDF`, TIMEOUT_MS);
76
+ const groups = data?.relatedGroup?.conceptGroup ?? [];
77
+ const g = groups.find((x) => x.tty === "SBDF");
78
+ if (!g || !g.conceptProperties) return [];
79
+ const brands = [];
80
+ for (const c of g.conceptProperties) {
81
+ const m = c.name.match(/\[([^\]]+)\]\s*$/);
82
+ if (!m) continue;
83
+ const prefix = c.name.slice(0, c.name.lastIndexOf("["));
84
+ if (prefix.includes("/")) continue;
85
+ brands.push(m[1].trim());
86
+ }
87
+ return uniq(brands).sort();
88
+ }
89
+ async function getDrugClassAndUses(rxcui) {
90
+ const data = await getJson(
91
+ `${RXNAV}/rxclass/class/byRxcui.json?rxcui=${rxcui}&relaSource=MEDRT`,
92
+ TIMEOUT_MS
93
+ );
94
+ const items = data?.rxclassDrugInfoList?.rxclassDrugInfo ?? [];
95
+ const byType = (t) => {
96
+ const hit = items.find((i) => i.rxclassMinConceptItem.classType === t);
97
+ return hit ? hit.rxclassMinConceptItem.className : null;
98
+ };
99
+ const drugClass = byType("EPC") || byType("MOA") || byType("CHEM");
100
+ const uses = uniq(
101
+ items.filter((i) => i.rela === "may_treat").map((i) => i.rxclassMinConceptItem.className)
102
+ ).slice(0, 6);
103
+ return { drugClass, uses };
104
+ }
105
+ async function resolveViaRxNorm(name) {
106
+ const rxcui = await findRxcui(name);
107
+ if (!rxcui) return null;
108
+ const [props, allRelated] = await Promise.all([
109
+ getJson(`${RXNAV}/rxcui/${rxcui}/properties.json`, TIMEOUT_MS),
110
+ getJson(`${RXNAV}/rxcui/${rxcui}/allrelated.json`, TIMEOUT_MS)
111
+ ]);
112
+ const tty = props?.properties?.tty;
113
+ const canonicalName = props?.properties?.name ?? name;
114
+ if (!tty) return null;
115
+ const inNames = collectNames(allRelated, "IN");
116
+ const minNames = collectNames(allRelated, "MIN");
117
+ const isBrand = tty === "BN" || tty === "SBD" || tty === "BPCK";
118
+ const inputType = isBrand ? "brand" : "generic";
119
+ let brandNames = null;
120
+ if (!isBrand) {
121
+ const single = await getSingleIngredientBrands(rxcui);
122
+ const fallback = uniq([...collectNames(allRelated, "BN"), ...collectNames(allRelated, "BPCK")]).sort();
123
+ brandNames = single.length ? single : fallback;
124
+ }
125
+ let genericName = null;
126
+ if (minNames.length) genericName = titleCase(minNames[0]);
127
+ else if (inNames.length === 1) genericName = titleCase(inNames[0]);
128
+ else if (inNames.length > 1) genericName = titleCase(uniq(inNames).join(" / "));
129
+ const classRxcui = !isBrand ? rxcui : (() => {
130
+ const inGroup = (allRelated?.allRelatedGroup?.conceptGroup ?? []).find((x) => x.tty === "IN");
131
+ return inGroup?.conceptProperties?.[0]?.rxcui ?? rxcui;
132
+ })();
133
+ const { drugClass, uses } = await getDrugClassAndUses(classRxcui);
134
+ return {
135
+ inputType,
136
+ inputName: titleCase(canonicalName),
137
+ genericName,
138
+ brandNames,
139
+ drugClass,
140
+ uses,
141
+ rxcui,
142
+ source: "RxNorm (NLM)",
143
+ regionNote: REGION_NOTE
144
+ };
145
+ }
146
+ async function resolveViaOpenFda(name) {
147
+ const q = name.replace(/"/g, "");
148
+ const search = `(openfda.brand_name:"${q}"+OR+openfda.generic_name:"${q}")`;
149
+ const url = `${OPENFDA}?search=${encodeURIComponent(search).replace(/%2B/g, "+")}&limit=1`;
150
+ const data = await getJson(url, TIMEOUT_MS);
151
+ const fda = data?.results?.[0]?.openfda;
152
+ if (!fda) return null;
153
+ const brand = (fda.brand_name ?? [])[0] ?? "";
154
+ const generic = (fda.generic_name ?? [])[0] ?? "";
155
+ if (!brand && !generic) return null;
156
+ const isBrand = brand.toLowerCase() === name.toLowerCase();
157
+ return {
158
+ inputType: isBrand ? "brand" : "generic",
159
+ inputName: titleCase(name),
160
+ genericName: generic ? titleCase(generic) : null,
161
+ brandNames: isBrand ? null : uniq(fda.brand_name ?? []).sort(),
162
+ drugClass: (fda.pharm_class_epc ?? [])[0] ?? (fda.pharm_class_moa ?? [])[0] ?? null,
163
+ uses: [],
164
+ rxcui: null,
165
+ source: "openFDA",
166
+ regionNote: REGION_NOTE
167
+ };
168
+ }
169
+ async function resolveName(name) {
170
+ if (!name || !name.trim()) return null;
171
+ const clean = name.trim();
172
+ const viaRx = await resolveViaRxNorm(clean).catch(() => null);
173
+ if (viaRx && (viaRx.genericName || viaRx.brandNames && viaRx.brandNames.length)) {
174
+ return viaRx;
175
+ }
176
+ return await resolveViaOpenFda(clean).catch(() => null);
177
+ }
178
+
179
+ // src/labels.ts
180
+ import * as cheerio from "cheerio";
181
+ var OPENFDA_URL = "https://api.fda.gov/drug/label.json";
182
+ var EMC_BASE = "https://www.medicines.org.uk";
183
+ async function getUspi(drug) {
184
+ if (!drug) return null;
185
+ const q = drug.trim().replace(/"/g, "");
186
+ const search = `(openfda.generic_name:"${q}"+OR+openfda.brand_name:"${q}")`;
187
+ const url = `${OPENFDA_URL}?search=${encodeURIComponent(search).replace(/%2B/g, "+")}&sort=effective_time:desc&limit=5`;
188
+ const data = await getJson(url);
189
+ const results = data?.results ?? [];
190
+ if (results.length === 0) return null;
191
+ const ranked = results.slice().sort((a, b) => score(b, q) - score(a, q));
192
+ const r = ranked[0];
193
+ const dosage = cleanSection(r.dosage_and_administration);
194
+ const boxed = cleanSection(r.boxed_warning);
195
+ const warn = cleanSection(r.warnings_and_cautions ?? r.warnings);
196
+ const warnings = [boxed ? `BOXED WARNING \u2014 ${boxed}` : "", warn].filter(Boolean).join("\n\n");
197
+ const adverse = cleanSection(r.adverse_reactions);
198
+ const indications = cleanSection(r.indications_and_usage);
199
+ const contraindications = cleanSection(r.contraindications);
200
+ if (!dosage && !warnings && !adverse && !indications && !contraindications) return null;
201
+ const setId = r.openfda?.spl_set_id?.[0] ?? "";
202
+ const brand = r.openfda?.brand_name?.[0] ?? "";
203
+ const generic = r.openfda?.generic_name?.[0] ?? q;
204
+ return {
205
+ region: "US",
206
+ authority: "FDA Prescribing Information",
207
+ productName: brand ? `${brand} (${generic})` : generic,
208
+ indications: indications || null,
209
+ dosage: dosage || null,
210
+ contraindications: contraindications || null,
211
+ warnings: warnings || null,
212
+ adverse: adverse || null,
213
+ updated: formatFdaDate(r.effective_time),
214
+ sourceUrl: setId ? `https://dailymed.nlm.nih.gov/dailymed/drugInfo.cfm?setid=${setId}` : "https://dailymed.nlm.nih.gov/dailymed/",
215
+ sourceLabel: "DailyMed (U.S. National Library of Medicine)"
216
+ };
217
+ }
218
+ function score(r, q) {
219
+ const g = (r.openfda?.generic_name ?? []).join(", ").toLowerCase();
220
+ let s = 0;
221
+ if (g && g.split(/,| and /).length === 1) s += 2;
222
+ if (g.includes(q.toLowerCase())) s += 1;
223
+ return s;
224
+ }
225
+ async function searchEmc(drug) {
226
+ const url = `${EMC_BASE}/emc/search?${new URLSearchParams({ q: drug })}`;
227
+ const res = await timedFetch(url);
228
+ if (!res.ok) return [];
229
+ const html = await res.text();
230
+ const $ = cheerio.load(html);
231
+ const out = [];
232
+ $("a[href*='/emc/product/']").each((_, el) => {
233
+ const href = $(el).attr("href") || "";
234
+ const m = href.match(/\/emc\/product\/(\d+)\/smpc/);
235
+ if (!m) return;
236
+ const name = $(el).text().trim();
237
+ if (!name || name.toLowerCase().includes("health professional")) return;
238
+ if (out.some((x) => x.product_id === m[1])) return;
239
+ out.push({ product_id: m[1], name });
240
+ });
241
+ return out;
242
+ }
243
+ var SMPC_WANT = {
244
+ "4.1": "indications",
245
+ "4.2": "dosage",
246
+ "4.3": "contraindications",
247
+ "4.4": "warnings",
248
+ "4.8": "adverse"
249
+ };
250
+ var SMPC_SECTION_NAMES = {
251
+ "4.1": "Therapeutic indications",
252
+ "4.2": "Posology and method of administration",
253
+ "4.3": "Contraindications",
254
+ "4.4": "Special warnings and precautions for use",
255
+ "4.5": "Interaction with other medicinal products",
256
+ "4.6": "Fertility, pregnancy and lactation",
257
+ "4.7": "Effects on ability to drive and use machines",
258
+ "4.8": "Undesirable effects",
259
+ "4.9": "Overdose",
260
+ "5.1": "Pharmacodynamic properties",
261
+ "5.2": "Pharmacokinetic properties",
262
+ "5.3": "Preclinical safety data"
263
+ };
264
+ function htmlToText(html) {
265
+ const $ = cheerio.load(html);
266
+ $("br").replaceWith("\n");
267
+ $("li").each((_, el) => {
268
+ $(el).prepend("- ");
269
+ $(el).append("\n");
270
+ });
271
+ $("p, div, tr, h1, h2, h3, h4, h5, h6").each((_, el) => {
272
+ $(el).append("\n");
273
+ });
274
+ return $.text().replace(/\n{3,}/g, "\n\n").trim();
275
+ }
276
+ function parseSmpcSections(html) {
277
+ const $ = cheerio.load(html);
278
+ const sectionElements = [];
279
+ $("[id*='SECTION'], [id*='section'], .sectionHeading, h2, h3, h4").each((_, el) => {
280
+ const text = $(el).text().trim();
281
+ const match = text.match(/^(\d+\.?\d*)\s+(.+)/);
282
+ if (match) sectionElements.push({ code: match[1], title: match[2].trim(), element: $(el) });
283
+ });
284
+ if (sectionElements.length === 0) {
285
+ $("*").each((_, el) => {
286
+ const $el = $(el);
287
+ if ($el.children().length > 0 && !$el.is("a, span, strong, em, b, i")) return;
288
+ const text = $el.text().trim();
289
+ const match = text.match(/^(\d+\.?\d*)\s+(.+)/);
290
+ if (match && SMPC_SECTION_NAMES[match[1]]) {
291
+ sectionElements.push({ code: match[1], title: match[2].trim(), element: $el });
292
+ }
293
+ });
294
+ }
295
+ const fields = {};
296
+ for (let i = 0; i < sectionElements.length; i++) {
297
+ const current = sectionElements[i];
298
+ const field = SMPC_WANT[current.code];
299
+ if (!field) continue;
300
+ const next = sectionElements[i + 1];
301
+ let content = "";
302
+ let node = current.element.next();
303
+ while (node.length > 0) {
304
+ if (next && node.is(next.element)) break;
305
+ const nodeHtml = $.html(node);
306
+ if (nodeHtml) content += htmlToText(nodeHtml) + "\n";
307
+ node = node.next();
308
+ }
309
+ if (!content.trim()) {
310
+ const parentHtml = $.html(current.element.parent());
311
+ if (parentHtml) {
312
+ content = htmlToText(parentHtml).replace(current.element.text().trim(), "").trim();
313
+ }
314
+ }
315
+ fields[field] = cleanSection(content);
316
+ }
317
+ return fields;
318
+ }
319
+ async function getSmpc(drug) {
320
+ if (!drug) return null;
321
+ try {
322
+ const matches = await searchEmc(drug);
323
+ if (matches.length === 0) return null;
324
+ const q = drug.toLowerCase();
325
+ matches.sort(
326
+ (a, b) => (b.name.toLowerCase().includes(q) ? 1 : 0) - (a.name.toLowerCase().includes(q) ? 1 : 0)
327
+ );
328
+ const best = matches[0];
329
+ const res = await timedFetch(`${EMC_BASE}/emc/product/${best.product_id}/smpc`);
330
+ if (!res.ok) return null;
331
+ const html = await res.text();
332
+ const sec = parseSmpcSections(html);
333
+ if (!sec.dosage && !sec.warnings && !sec.adverse && !sec.indications && !sec.contraindications)
334
+ return null;
335
+ return {
336
+ region: "UK/EU",
337
+ authority: "Summary of Product Characteristics (SmPC)",
338
+ productName: best.name,
339
+ indications: sec.indications || null,
340
+ dosage: sec.dosage || null,
341
+ contraindications: sec.contraindications || null,
342
+ warnings: sec.warnings || null,
343
+ adverse: sec.adverse || null,
344
+ updated: "",
345
+ sourceUrl: `${EMC_BASE}/emc/product/${best.product_id}/smpc`,
346
+ sourceLabel: "electronic medicines compendium (emc), UK"
347
+ };
348
+ } catch {
349
+ return null;
350
+ }
351
+ }
352
+ async function getLabels(drug) {
353
+ const [us, uk] = await Promise.all([
354
+ getUspi(drug).catch(() => null),
355
+ getSmpc(drug).catch(() => null)
356
+ ]);
357
+ return { us, uk };
358
+ }
359
+
360
+ // src/compare.ts
361
+ var SECTION_MAP = [
362
+ { topic: "Indications", field: "indications" },
363
+ { topic: "Dosing", field: "dosage" },
364
+ { topic: "Contraindications", field: "contraindications" },
365
+ { topic: "Warnings", field: "warnings" },
366
+ { topic: "Adverse Reactions", field: "adverse" }
367
+ ];
368
+ async function compareLabels(drug) {
369
+ const { us, uk } = await getLabels(drug);
370
+ const comparisons = SECTION_MAP.map(({ topic, field }) => ({
371
+ topic,
372
+ us: us ? us[field] : null,
373
+ uk: uk ? uk[field] : null
374
+ })).filter((c) => c.us || c.uk);
375
+ return {
376
+ drug,
377
+ comparisons,
378
+ usSource: us?.sourceUrl ?? null,
379
+ ukSource: uk?.sourceUrl ?? null
380
+ };
381
+ }
382
+ export {
383
+ SECTION_MAP,
384
+ cleanSection,
385
+ compareLabels,
386
+ formatFdaDate,
387
+ getLabels,
388
+ getSmpc,
389
+ getUspi,
390
+ resolveName
391
+ };
392
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/fetch.ts","../src/names.ts","../src/labels.ts","../src/compare.ts"],"sourcesContent":["/**\n * Shared HTTP + text helpers. Uses the runtime's global `fetch` (Node >=18),\n * so the package has no node-fetch dependency.\n */\n\nconst DEFAULT_TIMEOUT_MS = 12000;\nconst MAX_FIELD_CHARS = 1400;\n\nexport const USER_AGENT = \"drug-data/0.1 (pharmatools.ai; nick@pharmatools.ai)\";\n\nexport async function timedFetch(\n url: string,\n opts: RequestInit = {},\n timeoutMs = DEFAULT_TIMEOUT_MS\n): Promise<Response> {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n try {\n return await fetch(url, {\n ...opts,\n signal: controller.signal,\n headers: { \"User-Agent\": USER_AGENT, ...((opts.headers as Record<string, string>) || {}) },\n });\n } finally {\n clearTimeout(timer);\n }\n}\n\nexport async function getJson<T = unknown>(\n url: string,\n timeoutMs = DEFAULT_TIMEOUT_MS\n): Promise<T | null> {\n try {\n const res = await timedFetch(url, {}, timeoutMs);\n if (!res.ok) return null;\n return (await res.json()) as T;\n } catch {\n return null;\n }\n}\n\n/**\n * Tidy a raw label section: join arrays, strip a leading numbered ALL-CAPS\n * heading, collapse whitespace, and truncate on a sentence boundary.\n */\nexport function cleanSection(value: unknown, maxChars = MAX_FIELD_CHARS): string {\n if (!value) return \"\";\n let text = Array.isArray(value) ? value.join(\"\\n\\n\") : String(value);\n\n text = text\n .replace(/\\r/g, \"\")\n .replace(/[ \\t]+/g, \" \")\n .replace(/\\n{3,}/g, \"\\n\\n\")\n .trim();\n\n // Drop a leading section header line like \"5 WARNINGS AND PRECAUTIONS\"\n text = text.replace(/^\\s*\\d+(\\.\\d+)*\\s+[A-Z][A-Z \\-/,&]+\\n+/, \"\").trim();\n\n if (text.length > maxChars) {\n const slice = text.slice(0, maxChars);\n const lastStop = Math.max(slice.lastIndexOf(\". \"), slice.lastIndexOf(\"\\n\"));\n text = (lastStop > maxChars * 0.6 ? slice.slice(0, lastStop + 1) : slice).trim() + \" …\";\n }\n return text;\n}\n\nexport function formatFdaDate(yyyymmdd?: string): string {\n if (!yyyymmdd || !/^\\d{8}$/.test(yyyymmdd)) return \"\";\n const d = new Date(`${yyyymmdd.slice(0, 4)}-${yyyymmdd.slice(4, 6)}-${yyyymmdd.slice(6, 8)}`);\n if (isNaN(d.getTime())) return \"\";\n return d.toLocaleDateString(\"en-US\", { year: \"numeric\", month: \"short\", day: \"numeric\" });\n}\n\nexport function titleCase(s: string): string {\n return String(s || \"\").replace(/\\w\\S*/g, (t) => t.charAt(0).toUpperCase() + t.slice(1).toLowerCase());\n}\n\nexport function uniq<T>(arr: T[]): T[] {\n return [...new Set(arr.filter(Boolean))];\n}\n","/**\n * Deterministic brand <-> generic resolution via RxNorm/RxNav, with an openFDA\n * fallback. No AI, no hallucinated names. Brand names are US-registered;\n * international brand names are intentionally not invented.\n */\n\nimport { getJson, titleCase, uniq } from \"./fetch.js\";\nimport type { ResolveResult } from \"./types.js\";\n\nconst RXNAV = \"https://rxnav.nlm.nih.gov/REST\";\nconst OPENFDA = \"https://api.fda.gov/drug/label.json\";\nconst TIMEOUT_MS = 10000;\n\nconst REGION_NOTE = \"Brand names shown are those registered in the United States.\";\n\n/* ----------------------- RxNorm resolution ----------------------- */\n\nasync function findRxcui(name: string): Promise<string | null> {\n const exact = await getJson<any>(`${RXNAV}/rxcui.json?name=${encodeURIComponent(name)}`, TIMEOUT_MS);\n const id = exact?.idGroup?.rxnormId?.[0];\n if (id) return id;\n\n const approx = await getJson<any>(\n `${RXNAV}/approximateTerm.json?term=${encodeURIComponent(name)}&maxEntries=1`,\n TIMEOUT_MS\n );\n const cand = approx?.approximateGroup?.candidate?.[0];\n return cand ? cand.rxcui : null;\n}\n\nfunction collectNames(allRelated: any, tty: string): string[] {\n const groups = allRelated?.allRelatedGroup?.conceptGroup ?? [];\n const g = groups.find((x: any) => x.tty === tty);\n if (!g || !g.conceptProperties) return [];\n return g.conceptProperties.map((c: any) => c.name);\n}\n\n/**\n * Single-ingredient brand names for a generic ingredient, via SBDF concepts\n * whose name is \"<ingredient> <dose form> [Brand]\". Combination products read\n * \"<ing A> / <ing B> ... [Brand]\", so any \"/\" before the bracket is excluded.\n */\nasync function getSingleIngredientBrands(ingredientRxcui: string): Promise<string[]> {\n const data = await getJson<any>(`${RXNAV}/rxcui/${ingredientRxcui}/related.json?tty=SBDF`, TIMEOUT_MS);\n const groups = data?.relatedGroup?.conceptGroup ?? [];\n const g = groups.find((x: any) => x.tty === \"SBDF\");\n if (!g || !g.conceptProperties) return [];\n const brands: string[] = [];\n for (const c of g.conceptProperties) {\n const m = (c.name as string).match(/\\[([^\\]]+)\\]\\s*$/);\n if (!m) continue;\n const prefix = (c.name as string).slice(0, (c.name as string).lastIndexOf(\"[\"));\n if (prefix.includes(\"/\")) continue; // combination product — skip\n brands.push(m[1].trim());\n }\n return uniq(brands).sort();\n}\n\nasync function getDrugClassAndUses(\n rxcui: string\n): Promise<{ drugClass: string | null; uses: string[] }> {\n const data = await getJson<any>(\n `${RXNAV}/rxclass/class/byRxcui.json?rxcui=${rxcui}&relaSource=MEDRT`,\n TIMEOUT_MS\n );\n const items = data?.rxclassDrugInfoList?.rxclassDrugInfo ?? [];\n\n const byType = (t: string): string | null => {\n const hit = items.find((i: any) => i.rxclassMinConceptItem.classType === t);\n return hit ? hit.rxclassMinConceptItem.className : null;\n };\n // Prefer FDA Established Pharmacologic Class, then mechanism, then chemical.\n const drugClass = byType(\"EPC\") || byType(\"MOA\") || byType(\"CHEM\");\n\n const uses = uniq<string>(\n items\n .filter((i: any) => i.rela === \"may_treat\")\n .map((i: any) => i.rxclassMinConceptItem.className as string)\n ).slice(0, 6);\n\n return { drugClass, uses };\n}\n\nasync function resolveViaRxNorm(name: string): Promise<ResolveResult | null> {\n const rxcui = await findRxcui(name);\n if (!rxcui) return null;\n\n const [props, allRelated] = await Promise.all([\n getJson<any>(`${RXNAV}/rxcui/${rxcui}/properties.json`, TIMEOUT_MS),\n getJson<any>(`${RXNAV}/rxcui/${rxcui}/allrelated.json`, TIMEOUT_MS),\n ]);\n\n const tty: string | undefined = props?.properties?.tty;\n const canonicalName: string = props?.properties?.name ?? name;\n if (!tty) return null;\n\n const inNames = collectNames(allRelated, \"IN\");\n const minNames = collectNames(allRelated, \"MIN\");\n\n const isBrand = tty === \"BN\" || tty === \"SBD\" || tty === \"BPCK\";\n const inputType: \"brand\" | \"generic\" = isBrand ? \"brand\" : \"generic\";\n\n // For a generic, list only brands where it is the sole active ingredient.\n let brandNames: string[] | null = null;\n if (!isBrand) {\n const single = await getSingleIngredientBrands(rxcui);\n const fallback = uniq([...collectNames(allRelated, \"BN\"), ...collectNames(allRelated, \"BPCK\")]).sort();\n brandNames = single.length ? single : fallback;\n }\n\n // Generic display name: prefer the canonical combo string, else single, else joined.\n let genericName: string | null = null;\n if (minNames.length) genericName = titleCase(minNames[0]);\n else if (inNames.length === 1) genericName = titleCase(inNames[0]);\n else if (inNames.length > 1) genericName = titleCase(uniq(inNames).join(\" / \"));\n\n // Class + indications from the ingredient rxcui where possible.\n const classRxcui = !isBrand\n ? rxcui\n : (() => {\n const inGroup = (allRelated?.allRelatedGroup?.conceptGroup ?? []).find((x: any) => x.tty === \"IN\");\n return inGroup?.conceptProperties?.[0]?.rxcui ?? rxcui;\n })();\n const { drugClass, uses } = await getDrugClassAndUses(classRxcui);\n\n return {\n inputType,\n inputName: titleCase(canonicalName),\n genericName,\n brandNames,\n drugClass,\n uses,\n rxcui,\n source: \"RxNorm (NLM)\",\n regionNote: REGION_NOTE,\n };\n}\n\n/* ----------------------- openFDA fallback ------------------------ */\n\nasync function resolveViaOpenFda(name: string): Promise<ResolveResult | null> {\n const q = name.replace(/\"/g, \"\");\n const search = `(openfda.brand_name:\"${q}\"+OR+openfda.generic_name:\"${q}\")`;\n const url = `${OPENFDA}?search=${encodeURIComponent(search).replace(/%2B/g, \"+\")}&limit=1`;\n const data = await getJson<any>(url, TIMEOUT_MS);\n const fda = data?.results?.[0]?.openfda;\n if (!fda) return null;\n\n const brand = (fda.brand_name ?? [])[0] ?? \"\";\n const generic = (fda.generic_name ?? [])[0] ?? \"\";\n if (!brand && !generic) return null;\n\n const isBrand = brand.toLowerCase() === name.toLowerCase();\n return {\n inputType: isBrand ? \"brand\" : \"generic\",\n inputName: titleCase(name),\n genericName: generic ? titleCase(generic) : null,\n brandNames: isBrand ? null : uniq<string>(fda.brand_name ?? []).sort(),\n drugClass: (fda.pharm_class_epc ?? [])[0] ?? (fda.pharm_class_moa ?? [])[0] ?? null,\n uses: [],\n rxcui: null,\n source: \"openFDA\",\n regionNote: REGION_NOTE,\n };\n}\n\n/* --------------------------- public ----------------------------- */\n\n/**\n * Resolve a drug name deterministically. Tries RxNorm, then openFDA. Returns\n * null if neither source recognises the name.\n */\nexport async function resolveName(name: string): Promise<ResolveResult | null> {\n if (!name || !name.trim()) return null;\n const clean = name.trim();\n const viaRx = await resolveViaRxNorm(clean).catch(() => null);\n if (viaRx && (viaRx.genericName || (viaRx.brandNames && viaRx.brandNames.length))) {\n return viaRx;\n }\n return await resolveViaOpenFda(clean).catch(() => null);\n}\n","/**\n * Official drug-label lookups.\n * - US: FDA label via the openFDA API -> DailyMed citation\n * - UK/EU: SmPC via the eMC (medicines.org.uk) -> eMC citation\n *\n * Every network call is best-effort: any failure returns null so callers never\n * break. Pure fetch + parse — caching belongs to the consumer.\n */\n\nimport * as cheerio from \"cheerio\";\nimport { timedFetch, getJson, cleanSection, formatFdaDate } from \"./fetch.js\";\nimport type { RegionLabel, LabelResult } from \"./types.js\";\n\nconst OPENFDA_URL = \"https://api.fda.gov/drug/label.json\";\nconst EMC_BASE = \"https://www.medicines.org.uk\";\n\ninterface OpenFdaLabel {\n effective_time?: string;\n dosage_and_administration?: string[];\n boxed_warning?: string[];\n warnings_and_cautions?: string[];\n warnings?: string[];\n adverse_reactions?: string[];\n indications_and_usage?: string[];\n contraindications?: string[];\n openfda?: {\n generic_name?: string[];\n brand_name?: string[];\n spl_set_id?: string[];\n };\n}\ninterface OpenFdaResponse {\n results?: OpenFdaLabel[];\n}\n\n/* --------------------------- US: openFDA --------------------------- */\n\nexport async function getUspi(drug: string): Promise<RegionLabel | null> {\n if (!drug) return null;\n const q = drug.trim().replace(/\"/g, \"\");\n const search = `(openfda.generic_name:\"${q}\"+OR+openfda.brand_name:\"${q}\")`;\n const url = `${OPENFDA_URL}?search=${encodeURIComponent(search).replace(/%2B/g, \"+\")}&sort=effective_time:desc&limit=5`;\n\n const data = await getJson<OpenFdaResponse>(url);\n const results = data?.results ?? [];\n if (results.length === 0) return null;\n\n // Prefer a single-ingredient product over combination labels.\n const ranked = results.slice().sort((a, b) => score(b, q) - score(a, q));\n const r = ranked[0];\n\n const dosage = cleanSection(r.dosage_and_administration);\n const boxed = cleanSection(r.boxed_warning);\n const warn = cleanSection(r.warnings_and_cautions ?? r.warnings);\n const warnings = [boxed ? `BOXED WARNING — ${boxed}` : \"\", warn].filter(Boolean).join(\"\\n\\n\");\n const adverse = cleanSection(r.adverse_reactions);\n const indications = cleanSection(r.indications_and_usage);\n const contraindications = cleanSection(r.contraindications);\n\n if (!dosage && !warnings && !adverse && !indications && !contraindications) return null;\n\n const setId = r.openfda?.spl_set_id?.[0] ?? \"\";\n const brand = r.openfda?.brand_name?.[0] ?? \"\";\n const generic = r.openfda?.generic_name?.[0] ?? q;\n\n return {\n region: \"US\",\n authority: \"FDA Prescribing Information\",\n productName: brand ? `${brand} (${generic})` : generic,\n indications: indications || null,\n dosage: dosage || null,\n contraindications: contraindications || null,\n warnings: warnings || null,\n adverse: adverse || null,\n updated: formatFdaDate(r.effective_time),\n sourceUrl: setId\n ? `https://dailymed.nlm.nih.gov/dailymed/drugInfo.cfm?setid=${setId}`\n : \"https://dailymed.nlm.nih.gov/dailymed/\",\n sourceLabel: \"DailyMed (U.S. National Library of Medicine)\",\n };\n}\n\nfunction score(r: OpenFdaLabel, q: string): number {\n const g = (r.openfda?.generic_name ?? []).join(\", \").toLowerCase();\n let s = 0;\n if (g && g.split(/,| and /).length === 1) s += 2; // single ingredient\n if (g.includes(q.toLowerCase())) s += 1;\n return s;\n}\n\n/* --------------------------- UK/EU: eMC ---------------------------- */\n\ninterface EmcMatch {\n product_id: string;\n name: string;\n}\n\nasync function searchEmc(drug: string): Promise<EmcMatch[]> {\n const url = `${EMC_BASE}/emc/search?${new URLSearchParams({ q: drug })}`;\n const res = await timedFetch(url);\n if (!res.ok) return [];\n const html = await res.text();\n const $ = cheerio.load(html);\n const out: EmcMatch[] = [];\n $(\"a[href*='/emc/product/']\").each((_, el) => {\n const href = $(el).attr(\"href\") || \"\";\n const m = href.match(/\\/emc\\/product\\/(\\d+)\\/smpc/);\n if (!m) return;\n const name = $(el).text().trim();\n if (!name || name.toLowerCase().includes(\"health professional\")) return;\n if (out.some((x) => x.product_id === m[1])) return;\n out.push({ product_id: m[1], name });\n });\n return out;\n}\n\n// SmPC section number -> the field we store it under\nconst SMPC_WANT: Record<string, keyof Pick<RegionLabel, \"indications\" | \"dosage\" | \"contraindications\" | \"warnings\" | \"adverse\">> = {\n \"4.1\": \"indications\",\n \"4.2\": \"dosage\",\n \"4.3\": \"contraindications\",\n \"4.4\": \"warnings\",\n \"4.8\": \"adverse\",\n};\n\n// Canonical SmPC section titles — used as a fallback heading detector\nconst SMPC_SECTION_NAMES: Record<string, string> = {\n \"4.1\": \"Therapeutic indications\",\n \"4.2\": \"Posology and method of administration\",\n \"4.3\": \"Contraindications\",\n \"4.4\": \"Special warnings and precautions for use\",\n \"4.5\": \"Interaction with other medicinal products\",\n \"4.6\": \"Fertility, pregnancy and lactation\",\n \"4.7\": \"Effects on ability to drive and use machines\",\n \"4.8\": \"Undesirable effects\",\n \"4.9\": \"Overdose\",\n \"5.1\": \"Pharmacodynamic properties\",\n \"5.2\": \"Pharmacokinetic properties\",\n \"5.3\": \"Preclinical safety data\",\n};\n\nfunction htmlToText(html: string): string {\n const $ = cheerio.load(html);\n $(\"br\").replaceWith(\"\\n\");\n $(\"li\").each((_, el) => {\n $(el).prepend(\"- \");\n $(el).append(\"\\n\");\n });\n $(\"p, div, tr, h1, h2, h3, h4, h5, h6\").each((_, el) => {\n $(el).append(\"\\n\");\n });\n return $.text().replace(/\\n{3,}/g, \"\\n\\n\").trim();\n}\n\n/**\n * Parse the wanted SmPC sections from eMC HTML. Detect numbered headings, then\n * collect sibling content up to the next heading, with a parent-text fallback.\n */\nfunction parseSmpcSections(html: string): Record<string, string> {\n const $ = cheerio.load(html);\n const sectionElements: Array<{ code: string; title: string; element: any }> = [];\n\n // Strategy 1: headings whose text starts with a section number\n $(\"[id*='SECTION'], [id*='section'], .sectionHeading, h2, h3, h4\").each((_, el) => {\n const text = $(el).text().trim();\n const match = text.match(/^(\\d+\\.?\\d*)\\s+(.+)/);\n if (match) sectionElements.push({ code: match[1], title: match[2].trim(), element: $(el) });\n });\n\n // Strategy 2: fall back to scanning leaf nodes for known section headers\n if (sectionElements.length === 0) {\n $(\"*\").each((_, el) => {\n const $el = $(el);\n if ($el.children().length > 0 && !$el.is(\"a, span, strong, em, b, i\")) return;\n const text = $el.text().trim();\n const match = text.match(/^(\\d+\\.?\\d*)\\s+(.+)/);\n if (match && SMPC_SECTION_NAMES[match[1]]) {\n sectionElements.push({ code: match[1], title: match[2].trim(), element: $el });\n }\n });\n }\n\n const fields: Record<string, string> = {};\n for (let i = 0; i < sectionElements.length; i++) {\n const current = sectionElements[i];\n const field = SMPC_WANT[current.code];\n if (!field) continue;\n const next = sectionElements[i + 1];\n\n let content = \"\";\n let node = current.element.next();\n while (node.length > 0) {\n if (next && node.is(next.element)) break;\n const nodeHtml = $.html(node);\n if (nodeHtml) content += htmlToText(nodeHtml) + \"\\n\";\n node = node.next();\n }\n\n if (!content.trim()) {\n const parentHtml = $.html(current.element.parent());\n if (parentHtml) {\n content = htmlToText(parentHtml).replace(current.element.text().trim(), \"\").trim();\n }\n }\n\n fields[field] = cleanSection(content);\n }\n return fields;\n}\n\nexport async function getSmpc(drug: string): Promise<RegionLabel | null> {\n if (!drug) return null;\n try {\n const matches = await searchEmc(drug);\n if (matches.length === 0) return null;\n\n const q = drug.toLowerCase();\n matches.sort(\n (a, b) =>\n (b.name.toLowerCase().includes(q) ? 1 : 0) - (a.name.toLowerCase().includes(q) ? 1 : 0)\n );\n const best = matches[0];\n\n const res = await timedFetch(`${EMC_BASE}/emc/product/${best.product_id}/smpc`);\n if (!res.ok) return null;\n const html = await res.text();\n const sec = parseSmpcSections(html);\n\n if (!sec.dosage && !sec.warnings && !sec.adverse && !sec.indications && !sec.contraindications)\n return null;\n\n return {\n region: \"UK/EU\",\n authority: \"Summary of Product Characteristics (SmPC)\",\n productName: best.name,\n indications: sec.indications || null,\n dosage: sec.dosage || null,\n contraindications: sec.contraindications || null,\n warnings: sec.warnings || null,\n adverse: sec.adverse || null,\n updated: \"\",\n sourceUrl: `${EMC_BASE}/emc/product/${best.product_id}/smpc`,\n sourceLabel: \"electronic medicines compendium (emc), UK\",\n };\n } catch {\n return null;\n }\n}\n\n/* --------------------------- public API ---------------------------- */\n\n/** Look up both labels for a drug. Each side is null if unavailable. Never throws. */\nexport async function getLabels(drug: string): Promise<LabelResult> {\n const [us, uk] = await Promise.all([\n getUspi(drug).catch(() => null),\n getSmpc(drug).catch(() => null),\n ]);\n return { us, uk };\n}\n","/**\n * US (FDA) vs UK/EU (SmPC) side-by-side label comparison across mapped topics.\n */\n\nimport { getLabels } from \"./labels.js\";\nimport type { CompareResult, LabelComparison } from \"./types.js\";\n\ntype LabelField = \"indications\" | \"dosage\" | \"contraindications\" | \"warnings\" | \"adverse\";\n\n/** Topics compared across US and UK labels, mapped to the fields that hold them. */\nexport const SECTION_MAP: Array<{ topic: string; field: LabelField }> = [\n { topic: \"Indications\", field: \"indications\" },\n { topic: \"Dosing\", field: \"dosage\" },\n { topic: \"Contraindications\", field: \"contraindications\" },\n { topic: \"Warnings\", field: \"warnings\" },\n { topic: \"Adverse Reactions\", field: \"adverse\" },\n];\n\nexport async function compareLabels(drug: string): Promise<CompareResult> {\n const { us, uk } = await getLabels(drug);\n\n const comparisons: LabelComparison[] = SECTION_MAP.map(({ topic, field }) => ({\n topic,\n us: us ? us[field] : null,\n uk: uk ? uk[field] : null,\n })).filter((c) => c.us || c.uk);\n\n return {\n drug,\n comparisons,\n usSource: us?.sourceUrl ?? null,\n ukSource: uk?.sourceUrl ?? null,\n };\n}\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@pharmatools/drug-data",
3
+ "version": "0.1.0",
4
+ "description": "Deterministic drug-name resolution (RxNorm) and official label data (FDA openFDA + UK eMC SmPC), with US/UK comparison. The shared engine behind Pharmonym and PubCrawl.",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./dist/index.d.ts",
9
+ "import": "./dist/index.js",
10
+ "require": "./dist/index.cjs"
11
+ }
12
+ },
13
+ "main": "./dist/index.cjs",
14
+ "module": "./dist/index.js",
15
+ "types": "./dist/index.d.ts",
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "engines": {
20
+ "node": ">=18"
21
+ },
22
+ "scripts": {
23
+ "build": "tsup",
24
+ "test": "vitest run",
25
+ "typecheck": "tsc --noEmit",
26
+ "prepublishOnly": "npm run build && npm test"
27
+ },
28
+ "keywords": [
29
+ "pharma",
30
+ "drug-names",
31
+ "rxnorm",
32
+ "openfda",
33
+ "fda",
34
+ "smpc",
35
+ "dailymed",
36
+ "drug-labels"
37
+ ],
38
+ "author": "PharmaTools.AI",
39
+ "license": "MIT",
40
+ "dependencies": {
41
+ "cheerio": "^1.0.0"
42
+ },
43
+ "devDependencies": {
44
+ "tsup": "^8.0.0",
45
+ "typescript": "^5.4.0",
46
+ "vitest": "^2.0.0"
47
+ },
48
+ "publishConfig": {
49
+ "access": "public"
50
+ }
51
+ }