heor-agent-mcp 1.9.2 → 1.10.1
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/dist/providers/regulatory/ageRangeParser.d.ts +13 -0
- package/dist/providers/regulatory/ageRangeParser.d.ts.map +1 -0
- package/dist/providers/regulatory/ageRangeParser.js +78 -0
- package/dist/providers/regulatory/ageRangeParser.js.map +1 -0
- package/dist/providers/regulatory/autoCheck.d.ts +31 -0
- package/dist/providers/regulatory/autoCheck.d.ts.map +1 -0
- package/dist/providers/regulatory/autoCheck.js +93 -0
- package/dist/providers/regulatory/autoCheck.js.map +1 -0
- package/dist/providers/regulatory/cache.d.ts +37 -0
- package/dist/providers/regulatory/cache.d.ts.map +1 -0
- package/dist/providers/regulatory/cache.js +51 -0
- package/dist/providers/regulatory/cache.js.map +1 -0
- package/dist/providers/regulatory/dailymed.d.ts +34 -0
- package/dist/providers/regulatory/dailymed.d.ts.map +1 -0
- package/dist/providers/regulatory/dailymed.js +60 -0
- package/dist/providers/regulatory/dailymed.js.map +1 -0
- package/dist/providers/regulatory/drugNameNormaliser.d.ts +29 -0
- package/dist/providers/regulatory/drugNameNormaliser.d.ts.map +1 -0
- package/dist/providers/regulatory/drugNameNormaliser.js +186 -0
- package/dist/providers/regulatory/drugNameNormaliser.js.map +1 -0
- package/dist/providers/regulatory/emaEpi.d.ts +58 -0
- package/dist/providers/regulatory/emaEpi.d.ts.map +1 -0
- package/dist/providers/regulatory/emaEpi.js +105 -0
- package/dist/providers/regulatory/emaEpi.js.map +1 -0
- package/dist/providers/regulatory/index.d.ts +12 -0
- package/dist/providers/regulatory/index.d.ts.map +1 -0
- package/dist/providers/regulatory/index.js +12 -0
- package/dist/providers/regulatory/index.js.map +1 -0
- package/dist/providers/regulatory/openfda.d.ts +54 -0
- package/dist/providers/regulatory/openfda.d.ts.map +1 -0
- package/dist/providers/regulatory/openfda.js +216 -0
- package/dist/providers/regulatory/openfda.js.map +1 -0
- package/dist/providers/regulatory/types.d.ts +65 -0
- package/dist/providers/regulatory/types.d.ts.map +1 -0
- package/dist/providers/regulatory/types.js +8 -0
- package/dist/providers/regulatory/types.js.map +1 -0
- package/dist/providers/types.d.ts +2 -0
- package/dist/providers/types.d.ts.map +1 -1
- package/dist/server.js +16 -0
- package/dist/server.js.map +1 -1
- package/dist/tools/costEffectivenessModel.d.ts +30 -0
- package/dist/tools/costEffectivenessModel.d.ts.map +1 -1
- package/dist/tools/costEffectivenessModel.js +43 -1
- package/dist/tools/costEffectivenessModel.js.map +1 -1
- package/dist/tools/evidenceUnmetNeed.d.ts +40 -2
- package/dist/tools/evidenceUnmetNeed.d.ts.map +1 -1
- package/dist/tools/evidenceUnmetNeed.js +186 -12
- package/dist/tools/evidenceUnmetNeed.js.map +1 -1
- package/dist/tools/htaDossierPrep.d.ts.map +1 -1
- package/dist/tools/htaDossierPrep.js +97 -0
- package/dist/tools/htaDossierPrep.js.map +1 -1
- package/dist/tools/htaWorkflow.d.ts +44 -0
- package/dist/tools/htaWorkflow.d.ts.map +1 -1
- package/dist/tools/htaWorkflow.js +113 -1
- package/dist/tools/htaWorkflow.js.map +1 -1
- package/dist/tools/regulatoryStatusCheck.d.ts +60 -0
- package/dist/tools/regulatoryStatusCheck.d.ts.map +1 -0
- package/dist/tools/regulatoryStatusCheck.js +418 -0
- package/dist/tools/regulatoryStatusCheck.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Best-effort parser for label population text.
|
|
3
|
+
*
|
|
4
|
+
* Extracts { age_min_years, age_max_years, weight_constraint_kg, sex_constraint }
|
|
5
|
+
* from verbatim label text like:
|
|
6
|
+
* "pediatric patients aged 6 to 17 years weighing 45 kg or more"
|
|
7
|
+
*
|
|
8
|
+
* All fields are nullable — unparseable text returns all nulls.
|
|
9
|
+
* The caller always retains the verbatim population text.
|
|
10
|
+
*/
|
|
11
|
+
import type { PopulationParsed } from "./types.js";
|
|
12
|
+
export declare function parseAgeRange(text: string): PopulationParsed;
|
|
13
|
+
//# sourceMappingURL=ageRangeParser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ageRangeParser.d.ts","sourceRoot":"","sources":["../../../src/providers/regulatory/ageRangeParser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEnD,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,gBAAgB,CAqD5D"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Best-effort parser for label population text.
|
|
3
|
+
*
|
|
4
|
+
* Extracts { age_min_years, age_max_years, weight_constraint_kg, sex_constraint }
|
|
5
|
+
* from verbatim label text like:
|
|
6
|
+
* "pediatric patients aged 6 to 17 years weighing 45 kg or more"
|
|
7
|
+
*
|
|
8
|
+
* All fields are nullable — unparseable text returns all nulls.
|
|
9
|
+
* The caller always retains the verbatim population text.
|
|
10
|
+
*/
|
|
11
|
+
export function parseAgeRange(text) {
|
|
12
|
+
const result = {
|
|
13
|
+
age_min_years: null,
|
|
14
|
+
age_max_years: null,
|
|
15
|
+
weight_constraint_kg: null,
|
|
16
|
+
sex_constraint: null,
|
|
17
|
+
};
|
|
18
|
+
if (!text || typeof text !== "string")
|
|
19
|
+
return result;
|
|
20
|
+
const lower = text.toLowerCase();
|
|
21
|
+
// Sex constraint detection
|
|
22
|
+
if (/\b(females?|women|woman|premenopausal|postmenopausal)\b/.test(lower)) {
|
|
23
|
+
result.sex_constraint = "female_only";
|
|
24
|
+
}
|
|
25
|
+
else if (/\b(males?|men|man)\b/.test(lower)) {
|
|
26
|
+
result.sex_constraint = "male_only";
|
|
27
|
+
}
|
|
28
|
+
// Age range: "aged X to Y years" or "X to Y years"
|
|
29
|
+
const rangeMatch = text.match(/(?:aged?\s+)?(\d+)\s+to\s+(\d+)\s+years?/i);
|
|
30
|
+
if (rangeMatch) {
|
|
31
|
+
result.age_min_years = parseInt(rangeMatch[1], 10);
|
|
32
|
+
result.age_max_years = parseInt(rangeMatch[2], 10);
|
|
33
|
+
return _extractWeight(result, text);
|
|
34
|
+
}
|
|
35
|
+
// Minimum age patterns:
|
|
36
|
+
// "18 years of age or older"
|
|
37
|
+
// "6 years of age and older"
|
|
38
|
+
// "≥12 years" or ">= 12 years"
|
|
39
|
+
// "at least 12 years"
|
|
40
|
+
// "12 years and older"
|
|
41
|
+
const minAgePatterns = [
|
|
42
|
+
/(?:aged?\s+)?(\d+)\s+years?\s+(?:of\s+age\s+)?(?:and|or)\s+older/i,
|
|
43
|
+
/(?:aged?\s+)?(\d+)\s+years?\s+(?:of\s+age\s+)?(?:and|or)\s+above/i,
|
|
44
|
+
/(?:at\s+least\s+)?[≥>=]+\s*(\d+)\s+years?/i,
|
|
45
|
+
/patients?\s+(\d+)\s+years?\s+of\s+age\s+or\s+older/i,
|
|
46
|
+
/(?:adults?\s+)?\((\d+)\s+years?\s+and\s+older\)/i,
|
|
47
|
+
/(?:at\s+least\s+)(\d+)\s+years?\s+of\s+age/i,
|
|
48
|
+
/(?:aged?\s+)(\d+)\s+years?\s+(?:and\s+)?(?:and\s+)?older/i,
|
|
49
|
+
/\b(\d+)\s+years?\s+of\s+age\s+(?:and|or)\s+older/i,
|
|
50
|
+
];
|
|
51
|
+
for (const pattern of minAgePatterns) {
|
|
52
|
+
const m = text.match(pattern);
|
|
53
|
+
if (m) {
|
|
54
|
+
result.age_min_years = parseInt(m[1], 10);
|
|
55
|
+
return _extractWeight(result, text);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return _extractWeight(result, text);
|
|
59
|
+
}
|
|
60
|
+
function _extractWeight(result, text) {
|
|
61
|
+
// Weight patterns: "weighing 45 kg or more", "at least 30 kg body weight",
|
|
62
|
+
// "body weight ≥ 45 kg"
|
|
63
|
+
const weightPatterns = [
|
|
64
|
+
/weighing\s+(\d+(?:\.\d+)?)\s*kg/i,
|
|
65
|
+
/at\s+least\s+(\d+(?:\.\d+)?)\s*kg/i,
|
|
66
|
+
/[≥>=]+\s*(\d+(?:\.\d+)?)\s*kg/i,
|
|
67
|
+
/(\d+(?:\.\d+)?)\s*kg\s+or\s+(?:more|greater)/i,
|
|
68
|
+
];
|
|
69
|
+
for (const pattern of weightPatterns) {
|
|
70
|
+
const m = text.match(pattern);
|
|
71
|
+
if (m) {
|
|
72
|
+
result.weight_constraint_kg = parseFloat(m[1]);
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=ageRangeParser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ageRangeParser.js","sourceRoot":"","sources":["../../../src/providers/regulatory/ageRangeParser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,MAAM,MAAM,GAAqB;QAC/B,aAAa,EAAE,IAAI;QACnB,aAAa,EAAE,IAAI;QACnB,oBAAoB,EAAE,IAAI;QAC1B,cAAc,EAAE,IAAI;KACrB,CAAC;IAEF,IAAI,CAAC,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ;QAAE,OAAO,MAAM,CAAC;IAErD,MAAM,KAAK,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC;IAEjC,2BAA2B;IAC3B,IAAI,yDAAyD,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QAC1E,MAAM,CAAC,cAAc,GAAG,aAAa,CAAC;IACxC,CAAC;SAAM,IAAI,sBAAsB,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QAC9C,MAAM,CAAC,cAAc,GAAG,WAAW,CAAC;IACtC,CAAC;IAED,mDAAmD;IACnD,MAAM,UAAU,GAAG,IAAI,CAAC,KAAK,CAAC,2CAA2C,CAAC,CAAC;IAC3E,IAAI,UAAU,EAAE,CAAC;QACf,MAAM,CAAC,aAAa,GAAG,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QACnD,MAAM,CAAC,aAAa,GAAG,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QACnD,OAAO,cAAc,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IACtC,CAAC;IAED,wBAAwB;IACxB,6BAA6B;IAC7B,6BAA6B;IAC7B,+BAA+B;IAC/B,sBAAsB;IACtB,uBAAuB;IACvB,MAAM,cAAc,GAAG;QACrB,mEAAmE;QACnE,mEAAmE;QACnE,4CAA4C;QAC5C,qDAAqD;QACrD,kDAAkD;QAClD,6CAA6C;QAC7C,2DAA2D;QAC3D,mDAAmD;KACpD,CAAC;IAEF,KAAK,MAAM,OAAO,IAAI,cAAc,EAAE,CAAC;QACrC,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QAC9B,IAAI,CAAC,EAAE,CAAC;YACN,MAAM,CAAC,aAAa,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YAC1C,OAAO,cAAc,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;QACtC,CAAC;IACH,CAAC;IAED,OAAO,cAAc,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;AACtC,CAAC;AAED,SAAS,cAAc,CACrB,MAAwB,EACxB,IAAY;IAEZ,2EAA2E;IAC3E,0BAA0B;IAC1B,MAAM,cAAc,GAAG;QACrB,kCAAkC;QAClC,oCAAoC;QACpC,gCAAgC;QAChC,+CAA+C;KAChD,CAAC;IAEF,KAAK,MAAM,OAAO,IAAI,cAAc,EAAE,CAAC;QACrC,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QAC9B,IAAI,CAAC,EAAE,CAAC;YACN,MAAM,CAAC,oBAAoB,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YAC/C,MAAM;QACR,CAAC;IACH,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fan-out helper for auto-wiring regulatory.status_check into other tools.
|
|
3
|
+
*
|
|
4
|
+
* Design log #26. Used by evidence.unmet_need and hta_workflow Phase 3.6.
|
|
5
|
+
*
|
|
6
|
+
* Concurrency: up to 8 parallel calls (OpenFDA rate-limit guard).
|
|
7
|
+
* Cache: uses the shared 24h module-level cache inside regulatoryStatusCheck.ts.
|
|
8
|
+
* Never throws: each call is wrapped — one failure never aborts the batch.
|
|
9
|
+
*
|
|
10
|
+
* CRITICAL CYCLE-SAFETY: This module MUST NOT import from
|
|
11
|
+
* tools/evidenceUnmetNeed.ts or tools/htaWorkflow.ts (would create a cycle).
|
|
12
|
+
* Only imports from tools/regulatoryStatusCheck.ts (which owns no upstream deps).
|
|
13
|
+
*/
|
|
14
|
+
import type { RegulatoryStatusResult } from "./types.js";
|
|
15
|
+
export interface AutoCheckRequest {
|
|
16
|
+
drug: string;
|
|
17
|
+
region: "us" | "eu" | "uk" | "global";
|
|
18
|
+
indication?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface AutoCheckResult {
|
|
21
|
+
drug: string;
|
|
22
|
+
region: string;
|
|
23
|
+
result: RegulatoryStatusResult;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Fan-out helper for auto-wiring regulatory.status_check into other tools.
|
|
27
|
+
* Concurrency 8 to avoid OpenFDA rate-limit. Cache hits are free.
|
|
28
|
+
* Never throws — wraps each call so one failure doesn't kill the batch.
|
|
29
|
+
*/
|
|
30
|
+
export declare function autoCheckRegulatory(requests: AutoCheckRequest[]): Promise<AutoCheckResult[]>;
|
|
31
|
+
//# sourceMappingURL=autoCheck.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"autoCheck.d.ts","sourceRoot":"","sources":["../../../src/providers/regulatory/autoCheck.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAIzD,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,QAAQ,CAAC;IACtC,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,sBAAsB,CAAC;CAChC;AAwCD;;;;GAIG;AACH,wBAAsB,mBAAmB,CACvC,QAAQ,EAAE,gBAAgB,EAAE,GAC3B,OAAO,CAAC,eAAe,EAAE,CAAC,CA6C5B"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fan-out helper for auto-wiring regulatory.status_check into other tools.
|
|
3
|
+
*
|
|
4
|
+
* Design log #26. Used by evidence.unmet_need and hta_workflow Phase 3.6.
|
|
5
|
+
*
|
|
6
|
+
* Concurrency: up to 8 parallel calls (OpenFDA rate-limit guard).
|
|
7
|
+
* Cache: uses the shared 24h module-level cache inside regulatoryStatusCheck.ts.
|
|
8
|
+
* Never throws: each call is wrapped — one failure never aborts the batch.
|
|
9
|
+
*
|
|
10
|
+
* CRITICAL CYCLE-SAFETY: This module MUST NOT import from
|
|
11
|
+
* tools/evidenceUnmetNeed.ts or tools/htaWorkflow.ts (would create a cycle).
|
|
12
|
+
* Only imports from tools/regulatoryStatusCheck.ts (which owns no upstream deps).
|
|
13
|
+
*/
|
|
14
|
+
import { handleRegulatoryStatusCheck } from "../../tools/regulatoryStatusCheck.js";
|
|
15
|
+
// ── Constants ──────────────────────────────────────────────────────────────
|
|
16
|
+
const MAX_AUTO_REGULATORY_CALLS_PER_REQUEST = 8;
|
|
17
|
+
const CONCURRENCY_LIMIT = 8;
|
|
18
|
+
// ── Helper: build a graceful api_error result on exception ────────────────
|
|
19
|
+
function makeErrorResult(drug, region, message) {
|
|
20
|
+
return {
|
|
21
|
+
schema_version: "1.0",
|
|
22
|
+
drug,
|
|
23
|
+
drug_normalised_inn: drug.toLowerCase(),
|
|
24
|
+
drug_brand_names: [],
|
|
25
|
+
region,
|
|
26
|
+
current_status: "api_error",
|
|
27
|
+
approved_indications: [],
|
|
28
|
+
black_box_warnings: [],
|
|
29
|
+
rems_required: false,
|
|
30
|
+
contraindications: [],
|
|
31
|
+
recent_label_changes: {
|
|
32
|
+
count_12_months: 0,
|
|
33
|
+
last_revision_date: null,
|
|
34
|
+
last_revision_summary: null,
|
|
35
|
+
},
|
|
36
|
+
source_urls: [],
|
|
37
|
+
data_fetched_at: new Date().toISOString(),
|
|
38
|
+
data_age_hours: 0,
|
|
39
|
+
cache_hit: false,
|
|
40
|
+
api_error: {
|
|
41
|
+
source: "autoCheck",
|
|
42
|
+
message,
|
|
43
|
+
retry_after_seconds: 60,
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
// ── Fan-out with concurrency limit ─────────────────────────────────────────
|
|
48
|
+
/**
|
|
49
|
+
* Fan-out helper for auto-wiring regulatory.status_check into other tools.
|
|
50
|
+
* Concurrency 8 to avoid OpenFDA rate-limit. Cache hits are free.
|
|
51
|
+
* Never throws — wraps each call so one failure doesn't kill the batch.
|
|
52
|
+
*/
|
|
53
|
+
export async function autoCheckRegulatory(requests) {
|
|
54
|
+
if (requests.length === 0)
|
|
55
|
+
return [];
|
|
56
|
+
// Cap at MAX to prevent fan-out explosion
|
|
57
|
+
const capped = requests.slice(0, MAX_AUTO_REGULATORY_CALLS_PER_REQUEST);
|
|
58
|
+
// Process in batches of CONCURRENCY_LIMIT
|
|
59
|
+
const results = [];
|
|
60
|
+
for (let i = 0; i < capped.length; i += CONCURRENCY_LIMIT) {
|
|
61
|
+
const batch = capped.slice(i, i + CONCURRENCY_LIMIT);
|
|
62
|
+
const batchResults = await Promise.all(batch.map(async (req) => {
|
|
63
|
+
try {
|
|
64
|
+
const toolResult = await handleRegulatoryStatusCheck({
|
|
65
|
+
drug: req.drug,
|
|
66
|
+
region: req.region,
|
|
67
|
+
indication: req.indication,
|
|
68
|
+
});
|
|
69
|
+
// handleRegulatoryStatusCheck returns { content: string, audit }
|
|
70
|
+
const parsed = JSON.parse(typeof toolResult.content === "string"
|
|
71
|
+
? toolResult.content
|
|
72
|
+
: JSON.stringify(toolResult.content));
|
|
73
|
+
return {
|
|
74
|
+
drug: req.drug,
|
|
75
|
+
region: req.region,
|
|
76
|
+
result: parsed,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
catch (e) {
|
|
80
|
+
// Never throw — return graceful api_error
|
|
81
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
82
|
+
return {
|
|
83
|
+
drug: req.drug,
|
|
84
|
+
region: req.region,
|
|
85
|
+
result: makeErrorResult(req.drug, req.region, message),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
}));
|
|
89
|
+
results.push(...batchResults);
|
|
90
|
+
}
|
|
91
|
+
return results;
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=autoCheck.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"autoCheck.js","sourceRoot":"","sources":["../../../src/providers/regulatory/autoCheck.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 24h in-memory TTL cache for regulatory.status_check results.
|
|
3
|
+
*
|
|
4
|
+
* Design contract:
|
|
5
|
+
* - get() returns null on miss (expired or never set)
|
|
6
|
+
* - get() returns { value, cache_hit: true, data_age_hours } on hit
|
|
7
|
+
* - set() stores value with current timestamp
|
|
8
|
+
* - delete() removes key (used for force_refresh)
|
|
9
|
+
* - TTL configurable; default 24h
|
|
10
|
+
*/
|
|
11
|
+
interface CacheHit<T> {
|
|
12
|
+
value: T;
|
|
13
|
+
cache_hit: true;
|
|
14
|
+
data_age_hours: number;
|
|
15
|
+
}
|
|
16
|
+
export declare class RegulatoryCache<T = unknown> {
|
|
17
|
+
private readonly store;
|
|
18
|
+
private readonly ttlMs;
|
|
19
|
+
constructor(options?: {
|
|
20
|
+
ttlMs?: number;
|
|
21
|
+
});
|
|
22
|
+
/**
|
|
23
|
+
* Returns null on cache miss (expired or never set).
|
|
24
|
+
* Returns { value, cache_hit: true, data_age_hours } on hit.
|
|
25
|
+
*/
|
|
26
|
+
get(key: string): CacheHit<T> | null;
|
|
27
|
+
/**
|
|
28
|
+
* Stores value with current timestamp.
|
|
29
|
+
*/
|
|
30
|
+
set(key: string, value: T): void;
|
|
31
|
+
/**
|
|
32
|
+
* Removes key from cache (used by force_refresh logic).
|
|
33
|
+
*/
|
|
34
|
+
delete(key: string): void;
|
|
35
|
+
}
|
|
36
|
+
export {};
|
|
37
|
+
//# sourceMappingURL=cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../../src/providers/regulatory/cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAOH,UAAU,QAAQ,CAAC,CAAC;IAClB,KAAK,EAAE,CAAC,CAAC;IACT,SAAS,EAAE,IAAI,CAAC;IAChB,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,qBAAa,eAAe,CAAC,CAAC,GAAG,OAAO;IACtC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAoC;IAC1D,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;gBAEnB,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE;IAKxC;;;OAGG;IACH,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,QAAQ,CAAC,CAAC,CAAC,GAAG,IAAI;IAkBpC;;OAEG;IACH,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,GAAG,IAAI;IAIhC;;OAEG;IACH,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;CAG1B"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 24h in-memory TTL cache for regulatory.status_check results.
|
|
3
|
+
*
|
|
4
|
+
* Design contract:
|
|
5
|
+
* - get() returns null on miss (expired or never set)
|
|
6
|
+
* - get() returns { value, cache_hit: true, data_age_hours } on hit
|
|
7
|
+
* - set() stores value with current timestamp
|
|
8
|
+
* - delete() removes key (used for force_refresh)
|
|
9
|
+
* - TTL configurable; default 24h
|
|
10
|
+
*/
|
|
11
|
+
export class RegulatoryCache {
|
|
12
|
+
store = new Map();
|
|
13
|
+
ttlMs;
|
|
14
|
+
constructor(options) {
|
|
15
|
+
// Default 24h TTL
|
|
16
|
+
this.ttlMs = options?.ttlMs ?? 24 * 60 * 60 * 1000;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Returns null on cache miss (expired or never set).
|
|
20
|
+
* Returns { value, cache_hit: true, data_age_hours } on hit.
|
|
21
|
+
*/
|
|
22
|
+
get(key) {
|
|
23
|
+
const entry = this.store.get(key);
|
|
24
|
+
if (!entry)
|
|
25
|
+
return null;
|
|
26
|
+
const ageMs = Date.now() - entry.storedAt;
|
|
27
|
+
if (ageMs > this.ttlMs) {
|
|
28
|
+
// Expired — remove and return null
|
|
29
|
+
this.store.delete(key);
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
value: entry.value,
|
|
34
|
+
cache_hit: true,
|
|
35
|
+
data_age_hours: ageMs / (1000 * 60 * 60),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Stores value with current timestamp.
|
|
40
|
+
*/
|
|
41
|
+
set(key, value) {
|
|
42
|
+
this.store.set(key, { value, storedAt: Date.now() });
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Removes key from cache (used by force_refresh logic).
|
|
46
|
+
*/
|
|
47
|
+
delete(key) {
|
|
48
|
+
this.store.delete(key);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.js","sourceRoot":"","sources":["../../../src/providers/regulatory/cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAaH,MAAM,OAAO,eAAe;IACT,KAAK,GAAG,IAAI,GAAG,EAAyB,CAAC;IACzC,KAAK,CAAS;IAE/B,YAAY,OAA4B;QACtC,kBAAkB;QAClB,IAAI,CAAC,KAAK,GAAG,OAAO,EAAE,KAAK,IAAI,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;IACrD,CAAC;IAED;;;OAGG;IACH,GAAG,CAAC,GAAW;QACb,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAClC,IAAI,CAAC,KAAK;YAAE,OAAO,IAAI,CAAC;QAExB,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC,QAAQ,CAAC;QAC1C,IAAI,KAAK,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC;YACvB,mCAAmC;YACnC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YACvB,OAAO,IAAI,CAAC;QACd,CAAC;QAED,OAAO;YACL,KAAK,EAAE,KAAK,CAAC,KAAK;YAClB,SAAS,EAAE,IAAI;YACf,cAAc,EAAE,KAAK,GAAG,CAAC,IAAI,GAAG,EAAE,GAAG,EAAE,CAAC;SACzC,CAAC;IACJ,CAAC;IAED;;OAEG;IACH,GAAG,CAAC,GAAW,EAAE,KAAQ;QACvB,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;IACvD,CAAC;IAED;;OAEG;IACH,MAAM,CAAC,GAAW;QAChB,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IACzB,CAAC;CACF"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DailyMed v2 client — CROSS-CHECK / FALLBACK for US regulatory status.
|
|
3
|
+
*
|
|
4
|
+
* Phase 0 verified 2026-05-07:
|
|
5
|
+
* Search: GET https://dailymed.nlm.nih.gov/dailymed/services/v2/spls.json?drug_name={X}
|
|
6
|
+
* Returns: { data: [{spl_version, published_date, title, setid}], metadata }
|
|
7
|
+
*
|
|
8
|
+
* Used in v1 only to cross-check OpenFDA result freshness (compare
|
|
9
|
+
* published_date against OpenFDA's effective_time). Does NOT replace
|
|
10
|
+
* OpenFDA as the primary source.
|
|
11
|
+
*
|
|
12
|
+
* Design log #25.
|
|
13
|
+
*/
|
|
14
|
+
export interface DailyMedEntry {
|
|
15
|
+
setid: string;
|
|
16
|
+
spl_version: number;
|
|
17
|
+
published_date: string;
|
|
18
|
+
title: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Build the DailyMed search URL.
|
|
22
|
+
*/
|
|
23
|
+
export declare function buildDailyMedSearchUrl(drugName: string): string;
|
|
24
|
+
/**
|
|
25
|
+
* Parse DailyMed search response into structured entries.
|
|
26
|
+
*/
|
|
27
|
+
export declare function parseDailyMedSearchResponse(response: unknown): DailyMedEntry[];
|
|
28
|
+
/**
|
|
29
|
+
* Fetch DailyMed cross-check for a drug name.
|
|
30
|
+
* Returns null on any error (404, network failure, etc.) — DailyMed is
|
|
31
|
+
* non-critical; OpenFDA is the primary source.
|
|
32
|
+
*/
|
|
33
|
+
export declare function fetchDailyMedCrossCheck(drugName: string): Promise<DailyMedEntry[] | null>;
|
|
34
|
+
//# sourceMappingURL=dailymed.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dailymed.d.ts","sourceRoot":"","sources":["../../../src/providers/regulatory/dailymed.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAIH,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;CACf;AAgBD;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAG/D;AAED;;GAEG;AACH,wBAAgB,2BAA2B,CACzC,QAAQ,EAAE,OAAO,GAChB,aAAa,EAAE,CAYjB;AAED;;;;GAIG;AACH,wBAAsB,uBAAuB,CAC3C,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,aAAa,EAAE,GAAG,IAAI,CAAC,CAcjC"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DailyMed v2 client — CROSS-CHECK / FALLBACK for US regulatory status.
|
|
3
|
+
*
|
|
4
|
+
* Phase 0 verified 2026-05-07:
|
|
5
|
+
* Search: GET https://dailymed.nlm.nih.gov/dailymed/services/v2/spls.json?drug_name={X}
|
|
6
|
+
* Returns: { data: [{spl_version, published_date, title, setid}], metadata }
|
|
7
|
+
*
|
|
8
|
+
* Used in v1 only to cross-check OpenFDA result freshness (compare
|
|
9
|
+
* published_date against OpenFDA's effective_time). Does NOT replace
|
|
10
|
+
* OpenFDA as the primary source.
|
|
11
|
+
*
|
|
12
|
+
* Design log #25.
|
|
13
|
+
*/
|
|
14
|
+
const BASE_URL = "https://dailymed.nlm.nih.gov/dailymed/services/v2";
|
|
15
|
+
/**
|
|
16
|
+
* Build the DailyMed search URL.
|
|
17
|
+
*/
|
|
18
|
+
export function buildDailyMedSearchUrl(drugName) {
|
|
19
|
+
const encoded = encodeURIComponent(drugName);
|
|
20
|
+
return `${BASE_URL}/spls.json?drug_name=${encoded}`;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Parse DailyMed search response into structured entries.
|
|
24
|
+
*/
|
|
25
|
+
export function parseDailyMedSearchResponse(response) {
|
|
26
|
+
if (!response || typeof response !== "object")
|
|
27
|
+
return [];
|
|
28
|
+
const resp = response;
|
|
29
|
+
const data = resp.data;
|
|
30
|
+
if (!Array.isArray(data))
|
|
31
|
+
return [];
|
|
32
|
+
return data.map((entry) => ({
|
|
33
|
+
setid: entry.setid ?? "",
|
|
34
|
+
spl_version: entry.spl_version ?? 0,
|
|
35
|
+
published_date: entry.published_date ?? "",
|
|
36
|
+
title: entry.title ?? "",
|
|
37
|
+
}));
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Fetch DailyMed cross-check for a drug name.
|
|
41
|
+
* Returns null on any error (404, network failure, etc.) — DailyMed is
|
|
42
|
+
* non-critical; OpenFDA is the primary source.
|
|
43
|
+
*/
|
|
44
|
+
export async function fetchDailyMedCrossCheck(drugName) {
|
|
45
|
+
try {
|
|
46
|
+
const url = buildDailyMedSearchUrl(drugName);
|
|
47
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(15_000) });
|
|
48
|
+
if (!res.ok)
|
|
49
|
+
return null;
|
|
50
|
+
const data = (await res.json());
|
|
51
|
+
const entries = parseDailyMedSearchResponse(data);
|
|
52
|
+
if (entries.length === 0)
|
|
53
|
+
return null;
|
|
54
|
+
return entries;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=dailymed.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dailymed.js","sourceRoot":"","sources":["../../../src/providers/regulatory/dailymed.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,MAAM,QAAQ,GAAG,mDAAmD,CAAC;AAuBrE;;GAEG;AACH,MAAM,UAAU,sBAAsB,CAAC,QAAgB;IACrD,MAAM,OAAO,GAAG,kBAAkB,CAAC,QAAQ,CAAC,CAAC;IAC7C,OAAO,GAAG,QAAQ,wBAAwB,OAAO,EAAE,CAAC;AACtD,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,2BAA2B,CACzC,QAAiB;IAEjB,IAAI,CAAC,QAAQ,IAAI,OAAO,QAAQ,KAAK,QAAQ;QAAE,OAAO,EAAE,CAAC;IACzD,MAAM,IAAI,GAAG,QAAkC,CAAC;IAChD,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;IACvB,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,CAAC;IAEpC,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;QAC1B,KAAK,EAAE,KAAK,CAAC,KAAK,IAAI,EAAE;QACxB,WAAW,EAAE,KAAK,CAAC,WAAW,IAAI,CAAC;QACnC,cAAc,EAAE,KAAK,CAAC,cAAc,IAAI,EAAE;QAC1C,KAAK,EAAE,KAAK,CAAC,KAAK,IAAI,EAAE;KACzB,CAAC,CAAC,CAAC;AACN,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAC3C,QAAgB;IAEhB,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,sBAAsB,CAAC,QAAQ,CAAC,CAAC;QAC7C,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE,EAAE,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QACtE,IAAI,CAAC,GAAG,CAAC,EAAE;YAAE,OAAO,IAAI,CAAC;QAEzB,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAY,CAAC;QAC3C,MAAM,OAAO,GAAG,2BAA2B,CAAC,IAAI,CAAC,CAAC;QAClD,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC;QAEtC,OAAO,OAAO,CAAC;IACjB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drug name normaliser — INN / brand / biosimilar-suffix → canonical INN.
|
|
3
|
+
*
|
|
4
|
+
* Design log #25. Covers ~70 most-common HEOR-relevant molecules.
|
|
5
|
+
* Returns did_you_mean[] (Levenshtein-based top-3) when no match.
|
|
6
|
+
*
|
|
7
|
+
* Biosimilar suffix stripping: FDA 4-letter suffixes (e.g., -vfrm, -aooe, -adaz)
|
|
8
|
+
* are stripped to recover the INN stem.
|
|
9
|
+
*/
|
|
10
|
+
interface NormaliseResult {
|
|
11
|
+
inn: string | null;
|
|
12
|
+
did_you_mean?: string[];
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Normalises a drug name to canonical INN.
|
|
16
|
+
*
|
|
17
|
+
* Lookup priority:
|
|
18
|
+
* 1. INN exact match (case-insensitive)
|
|
19
|
+
* 2. Brand name exact match
|
|
20
|
+
* 3. Strip biosimilar suffix → INN match
|
|
21
|
+
* 4. Not found → null + did_you_mean
|
|
22
|
+
*/
|
|
23
|
+
export declare function normaliseDrugName(drug: string): NormaliseResult;
|
|
24
|
+
/**
|
|
25
|
+
* Returns all known brand names for a given INN.
|
|
26
|
+
*/
|
|
27
|
+
export declare function getBrandNames(inn: string): string[];
|
|
28
|
+
export {};
|
|
29
|
+
//# sourceMappingURL=drugNameNormaliser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"drugNameNormaliser.d.ts","sourceRoot":"","sources":["../../../src/providers/regulatory/drugNameNormaliser.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,UAAU,eAAe;IACvB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;CACzB;AAuID;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,eAAe,CAoC/D;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAKnD"}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drug name normaliser — INN / brand / biosimilar-suffix → canonical INN.
|
|
3
|
+
*
|
|
4
|
+
* Design log #25. Covers ~70 most-common HEOR-relevant molecules.
|
|
5
|
+
* Returns did_you_mean[] (Levenshtein-based top-3) when no match.
|
|
6
|
+
*
|
|
7
|
+
* Biosimilar suffix stripping: FDA 4-letter suffixes (e.g., -vfrm, -aooe, -adaz)
|
|
8
|
+
* are stripped to recover the INN stem.
|
|
9
|
+
*/
|
|
10
|
+
// INN → [brand names] mapping (case-insensitive on lookup)
|
|
11
|
+
const INN_TO_BRANDS = {
|
|
12
|
+
// CGRP antibodies
|
|
13
|
+
fremanezumab: ["ajovy"],
|
|
14
|
+
erenumab: ["aimovig"],
|
|
15
|
+
galcanezumab: ["emgality"],
|
|
16
|
+
eptinezumab: ["vyepti"],
|
|
17
|
+
// Alzheimer's
|
|
18
|
+
lecanemab: ["leqembi"],
|
|
19
|
+
donanemab: ["kisunla"],
|
|
20
|
+
aducanumab: ["aduhelm"],
|
|
21
|
+
// Diabetes / obesity
|
|
22
|
+
semaglutide: ["ozempic", "wegovy", "rybelsus"],
|
|
23
|
+
tirzepatide: ["mounjaro", "zepbound"],
|
|
24
|
+
dulaglutide: ["trulicity"],
|
|
25
|
+
liraglutide: ["victoza", "saxenda"],
|
|
26
|
+
dapagliflozin: ["farxiga", "forxiga"],
|
|
27
|
+
empagliflozin: ["jardiance"],
|
|
28
|
+
canagliflozin: ["invokana"],
|
|
29
|
+
// Oncology
|
|
30
|
+
pembrolizumab: ["keytruda"],
|
|
31
|
+
nivolumab: ["opdivo"],
|
|
32
|
+
atezolizumab: ["tecentriq"],
|
|
33
|
+
durvalumab: ["imfinzi"],
|
|
34
|
+
ipilimumab: ["yervoy"],
|
|
35
|
+
trastuzumab: ["herceptin"],
|
|
36
|
+
pertuzumab: ["perjeta"],
|
|
37
|
+
bevacizumab: ["avastin"],
|
|
38
|
+
rituximab: ["rituxan", "mabthera"],
|
|
39
|
+
cetuximab: ["erbitux"],
|
|
40
|
+
osimertinib: ["tagrisso"],
|
|
41
|
+
palbociclib: ["ibrance"],
|
|
42
|
+
ribociclib: ["kisqali"],
|
|
43
|
+
abemaciclib: ["verzenio"],
|
|
44
|
+
// Immunology / rheumatology
|
|
45
|
+
adalimumab: ["humira"],
|
|
46
|
+
etanercept: ["enbrel"],
|
|
47
|
+
infliximab: ["remicade"],
|
|
48
|
+
tocilizumab: ["actemra"],
|
|
49
|
+
sarilumab: ["kevzara"],
|
|
50
|
+
baricitinib: ["olumiant"],
|
|
51
|
+
tofacitinib: ["xeljanz"],
|
|
52
|
+
upadacitinib: ["rinvoq"],
|
|
53
|
+
secukinumab: ["cosentyx"],
|
|
54
|
+
ixekizumab: ["taltz"],
|
|
55
|
+
bimekizumab: ["bimzelx"],
|
|
56
|
+
risankizumab: ["skyrizi"],
|
|
57
|
+
guselkumab: ["tremfya"],
|
|
58
|
+
dupilumab: ["dupixent"],
|
|
59
|
+
tralokinumab: ["adbry"],
|
|
60
|
+
// Cardiology
|
|
61
|
+
evolocumab: ["repatha"],
|
|
62
|
+
alirocumab: ["praluent"],
|
|
63
|
+
inclisiran: ["leqvio"],
|
|
64
|
+
sacubitril: ["entresto"],
|
|
65
|
+
// Rare diseases
|
|
66
|
+
nusinersen: ["spinraza"],
|
|
67
|
+
risdiplam: ["evrysdi"],
|
|
68
|
+
onasemnogene: ["zolgensma"],
|
|
69
|
+
migalastat: ["galafold"],
|
|
70
|
+
ivacaftor: ["kalydeco"],
|
|
71
|
+
elexacaftor: ["trikafta"],
|
|
72
|
+
// Multiple sclerosis
|
|
73
|
+
natalizumab: ["tysabri"],
|
|
74
|
+
ocrelizumab: ["ocrevus"],
|
|
75
|
+
ofatumumab: ["kesimpta"],
|
|
76
|
+
ozanimod: ["zeposia"],
|
|
77
|
+
siponimod: ["mayzent"],
|
|
78
|
+
cladribine: ["mavenclad"],
|
|
79
|
+
// PV / migraine
|
|
80
|
+
lasmiditan: ["reyvow"],
|
|
81
|
+
rimegepant: ["nurtec"],
|
|
82
|
+
ubrogepant: ["ubrelvy"],
|
|
83
|
+
atogepant: ["qulipta"],
|
|
84
|
+
zavegepant: ["zavzpret"],
|
|
85
|
+
};
|
|
86
|
+
// Reverse map: brand → INN (built from INN_TO_BRANDS)
|
|
87
|
+
const BRAND_TO_INN = new Map();
|
|
88
|
+
for (const [inn, brands] of Object.entries(INN_TO_BRANDS)) {
|
|
89
|
+
for (const brand of brands) {
|
|
90
|
+
BRAND_TO_INN.set(brand.toLowerCase(), inn);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// Full INN list for Levenshtein "did you mean"
|
|
94
|
+
const ALL_INNS = Object.keys(INN_TO_BRANDS);
|
|
95
|
+
/**
|
|
96
|
+
* Strips FDA 4-letter biosimilar suffix (e.g., "fremanezumab-vfrm" → "fremanezumab").
|
|
97
|
+
* Returns the stem if the suffix matches /^-[a-z]{4}$/i, else returns the original.
|
|
98
|
+
*/
|
|
99
|
+
function stripBiosimilarSuffix(name) {
|
|
100
|
+
const match = name.match(/^(.+?)-([a-z]{4})$/i);
|
|
101
|
+
if (match) {
|
|
102
|
+
return match[1].toLowerCase();
|
|
103
|
+
}
|
|
104
|
+
return name.toLowerCase();
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Levenshtein distance (simple DP implementation).
|
|
108
|
+
*/
|
|
109
|
+
function levenshtein(a, b) {
|
|
110
|
+
const m = a.length;
|
|
111
|
+
const n = b.length;
|
|
112
|
+
const dp = Array.from({ length: m + 1 }, (_, i) => Array.from({ length: n + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0)));
|
|
113
|
+
for (let i = 1; i <= m; i++) {
|
|
114
|
+
for (let j = 1; j <= n; j++) {
|
|
115
|
+
if (a[i - 1] === b[j - 1]) {
|
|
116
|
+
dp[i][j] = dp[i - 1][j - 1];
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
dp[i][j] = 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return dp[m][n];
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Returns the top-3 closest INN names by Levenshtein distance.
|
|
127
|
+
*/
|
|
128
|
+
function findDidYouMean(query) {
|
|
129
|
+
const lower = query.toLowerCase();
|
|
130
|
+
return ALL_INNS
|
|
131
|
+
.map((inn) => ({ inn, dist: levenshtein(lower, inn) }))
|
|
132
|
+
.sort((a, b) => a.dist - b.dist)
|
|
133
|
+
.slice(0, 3)
|
|
134
|
+
.map((x) => x.inn);
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Normalises a drug name to canonical INN.
|
|
138
|
+
*
|
|
139
|
+
* Lookup priority:
|
|
140
|
+
* 1. INN exact match (case-insensitive)
|
|
141
|
+
* 2. Brand name exact match
|
|
142
|
+
* 3. Strip biosimilar suffix → INN match
|
|
143
|
+
* 4. Not found → null + did_you_mean
|
|
144
|
+
*/
|
|
145
|
+
export function normaliseDrugName(drug) {
|
|
146
|
+
if (!drug || typeof drug !== "string") {
|
|
147
|
+
return { inn: null, did_you_mean: findDidYouMean("") };
|
|
148
|
+
}
|
|
149
|
+
const lower = drug.trim().toLowerCase();
|
|
150
|
+
if (!lower) {
|
|
151
|
+
return { inn: null, did_you_mean: findDidYouMean("") };
|
|
152
|
+
}
|
|
153
|
+
// 1. Direct INN match
|
|
154
|
+
if (lower in INN_TO_BRANDS) {
|
|
155
|
+
return { inn: lower };
|
|
156
|
+
}
|
|
157
|
+
// 2. Brand name match
|
|
158
|
+
const fromBrand = BRAND_TO_INN.get(lower);
|
|
159
|
+
if (fromBrand) {
|
|
160
|
+
return { inn: fromBrand };
|
|
161
|
+
}
|
|
162
|
+
// 3. Strip biosimilar suffix and retry
|
|
163
|
+
const stem = stripBiosimilarSuffix(drug);
|
|
164
|
+
if (stem !== lower) {
|
|
165
|
+
if (stem in INN_TO_BRANDS) {
|
|
166
|
+
return { inn: stem };
|
|
167
|
+
}
|
|
168
|
+
// Also check if stem is a brand
|
|
169
|
+
const stemFromBrand = BRAND_TO_INN.get(stem);
|
|
170
|
+
if (stemFromBrand) {
|
|
171
|
+
return { inn: stemFromBrand };
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
// 4. Not found
|
|
175
|
+
return { inn: null, did_you_mean: findDidYouMean(lower) };
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Returns all known brand names for a given INN.
|
|
179
|
+
*/
|
|
180
|
+
export function getBrandNames(inn) {
|
|
181
|
+
const lower = inn.toLowerCase();
|
|
182
|
+
const brands = INN_TO_BRANDS[lower] ?? [];
|
|
183
|
+
// Capitalise first letter for display
|
|
184
|
+
return brands.map((b) => b.charAt(0).toUpperCase() + b.slice(1));
|
|
185
|
+
}
|
|
186
|
+
//# sourceMappingURL=drugNameNormaliser.js.map
|