justlabs-mcp-server 1.0.0 → 1.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/dist/__tests__/analysis.test.d.ts +5 -0
- package/dist/__tests__/analysis.test.js +203 -0
- package/dist/analysis/analyzePanelResults.d.ts +45 -0
- package/dist/analysis/analyzePanelResults.js +137 -0
- package/dist/analysis/generatePatientSummary.d.ts +25 -0
- package/dist/analysis/generatePatientSummary.js +94 -0
- package/dist/analysis/suggestNextSteps.d.ts +37 -0
- package/dist/analysis/suggestNextSteps.js +136 -0
- package/dist/index.js +164 -0
- package/dist/ranges/panels.d.ts +28 -0
- package/dist/ranges/panels.js +115 -0
- package/dist/ranges/questRanges.d.ts +53 -0
- package/dist/ranges/questRanges.js +335 -0
- package/package.json +2 -2
- package/src/__tests__/analysis.test.ts +235 -0
- package/src/analysis/analyzePanelResults.ts +205 -0
- package/src/analysis/generatePatientSummary.ts +131 -0
- package/src/analysis/suggestNextSteps.ts +183 -0
- package/src/index.ts +180 -0
- package/src/ranges/panels.ts +145 -0
- package/src/ranges/questRanges.ts +403 -0
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quest-style reference ranges for common biomarkers
|
|
3
|
+
* These are educational reference points, not diagnostic criteria
|
|
4
|
+
*/
|
|
5
|
+
export const CRITICAL_THRESHOLDS = [
|
|
6
|
+
{
|
|
7
|
+
key: "glucose",
|
|
8
|
+
criticalLow: 50,
|
|
9
|
+
criticalHigh: 400,
|
|
10
|
+
unit: "mg/dL",
|
|
11
|
+
message: "Glucose levels outside this range require prompt medical evaluation.",
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
key: "potassium",
|
|
15
|
+
criticalLow: 2.5,
|
|
16
|
+
criticalHigh: 6.5,
|
|
17
|
+
unit: "mEq/L",
|
|
18
|
+
message: "Potassium levels outside this range can affect heart rhythm and require immediate medical attention.",
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
key: "sodium",
|
|
22
|
+
criticalLow: 120,
|
|
23
|
+
criticalHigh: 160,
|
|
24
|
+
unit: "mEq/L",
|
|
25
|
+
message: "Sodium levels outside this range require prompt medical evaluation.",
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
key: "hemoglobin",
|
|
29
|
+
criticalLow: 7.0,
|
|
30
|
+
criticalHigh: 20.0,
|
|
31
|
+
unit: "g/dL",
|
|
32
|
+
message: "Hemoglobin levels outside this range require prompt medical evaluation.",
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
key: "tsh",
|
|
36
|
+
criticalLow: 0.01,
|
|
37
|
+
criticalHigh: 50,
|
|
38
|
+
unit: "mIU/L",
|
|
39
|
+
message: "TSH levels outside this range suggest significant thyroid dysfunction requiring medical evaluation.",
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
key: "creatinine",
|
|
43
|
+
criticalHigh: 10.0,
|
|
44
|
+
unit: "mg/dL",
|
|
45
|
+
message: "Significantly elevated creatinine may indicate kidney dysfunction requiring medical evaluation.",
|
|
46
|
+
},
|
|
47
|
+
];
|
|
48
|
+
export const BIOMARKER_RANGES = [
|
|
49
|
+
// Thyroid markers
|
|
50
|
+
{
|
|
51
|
+
key: "tsh",
|
|
52
|
+
displayName: "TSH (Thyroid Stimulating Hormone)",
|
|
53
|
+
unit: "mIU/L",
|
|
54
|
+
defaultRange: { low: 0.45, high: 4.5, unit: "mIU/L" },
|
|
55
|
+
educationalContext: "TSH is produced by the pituitary gland and signals the thyroid to produce hormones.",
|
|
56
|
+
lowContext: "Lower TSH may indicate the thyroid is producing more hormone than usual.",
|
|
57
|
+
highContext: "Higher TSH may indicate the thyroid is working harder than usual to produce hormones.",
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
key: "free-t4",
|
|
61
|
+
displayName: "Free T4 (Thyroxine)",
|
|
62
|
+
unit: "ng/dL",
|
|
63
|
+
defaultRange: { low: 0.8, high: 1.8, unit: "ng/dL" },
|
|
64
|
+
educationalContext: "Free T4 is the active, unbound form of the main thyroid hormone.",
|
|
65
|
+
lowContext: "Lower Free T4 may indicate reduced thyroid hormone production.",
|
|
66
|
+
highContext: "Higher Free T4 may indicate increased thyroid hormone production.",
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
key: "free-t3",
|
|
70
|
+
displayName: "Free T3 (Triiodothyronine)",
|
|
71
|
+
unit: "pg/mL",
|
|
72
|
+
defaultRange: { low: 2.3, high: 4.2, unit: "pg/mL" },
|
|
73
|
+
educationalContext: "Free T3 is the most active thyroid hormone used by cells.",
|
|
74
|
+
lowContext: "Lower Free T3 may indicate reduced conversion from T4 or lower thyroid function.",
|
|
75
|
+
highContext: "Higher Free T3 may indicate increased thyroid activity.",
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
key: "tpo-antibodies",
|
|
79
|
+
displayName: "TPO Antibodies",
|
|
80
|
+
unit: "IU/mL",
|
|
81
|
+
defaultRange: { high: 34, unit: "IU/mL" },
|
|
82
|
+
educationalContext: "TPO antibodies, when elevated, may indicate autoimmune thyroid activity.",
|
|
83
|
+
highContext: "Elevated TPO antibodies are often seen in autoimmune thyroid conditions like Hashimoto's.",
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
key: "thyroglobulin-antibodies",
|
|
87
|
+
displayName: "Thyroglobulin Antibodies",
|
|
88
|
+
unit: "IU/mL",
|
|
89
|
+
defaultRange: { high: 1, unit: "IU/mL" },
|
|
90
|
+
educationalContext: "Thyroglobulin antibodies, when elevated, may indicate autoimmune thyroid activity.",
|
|
91
|
+
highContext: "Elevated thyroglobulin antibodies are sometimes seen alongside TPO antibodies in autoimmune thyroid conditions.",
|
|
92
|
+
},
|
|
93
|
+
// Iron and blood markers
|
|
94
|
+
{
|
|
95
|
+
key: "ferritin",
|
|
96
|
+
displayName: "Ferritin",
|
|
97
|
+
unit: "ng/mL",
|
|
98
|
+
defaultRange: { low: 30, high: 300, unit: "ng/mL" },
|
|
99
|
+
maleRange: { low: 30, high: 400, unit: "ng/mL" },
|
|
100
|
+
femaleRange: { low: 20, high: 200, unit: "ng/mL" },
|
|
101
|
+
educationalContext: "Ferritin reflects iron stores in your body.",
|
|
102
|
+
lowContext: "Lower ferritin may indicate depleted iron stores, a common cause of fatigue.",
|
|
103
|
+
highContext: "Higher ferritin can indicate excess iron stores or inflammation.",
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
key: "hemoglobin",
|
|
107
|
+
displayName: "Hemoglobin",
|
|
108
|
+
unit: "g/dL",
|
|
109
|
+
defaultRange: { low: 12.0, high: 17.5, unit: "g/dL" },
|
|
110
|
+
maleRange: { low: 13.5, high: 17.5, unit: "g/dL" },
|
|
111
|
+
femaleRange: { low: 12.0, high: 16.0, unit: "g/dL" },
|
|
112
|
+
educationalContext: "Hemoglobin carries oxygen in red blood cells.",
|
|
113
|
+
lowContext: "Lower hemoglobin may indicate anemia, which can cause fatigue and weakness.",
|
|
114
|
+
highContext: "Higher hemoglobin may indicate dehydration or other conditions.",
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
key: "wbc",
|
|
118
|
+
displayName: "White Blood Cell Count",
|
|
119
|
+
unit: "K/uL",
|
|
120
|
+
defaultRange: { low: 4.5, high: 11.0, unit: "K/uL" },
|
|
121
|
+
educationalContext: "White blood cells are part of your immune system.",
|
|
122
|
+
lowContext: "Lower WBC may indicate reduced immune cell production.",
|
|
123
|
+
highContext: "Higher WBC may indicate the body is responding to infection or inflammation.",
|
|
124
|
+
},
|
|
125
|
+
// Vitamins
|
|
126
|
+
{
|
|
127
|
+
key: "vitamin-d",
|
|
128
|
+
displayName: "Vitamin D, 25-Hydroxy",
|
|
129
|
+
unit: "ng/mL",
|
|
130
|
+
defaultRange: { low: 30, high: 100, unit: "ng/mL" },
|
|
131
|
+
educationalContext: "Vitamin D is important for bone health, immune function, and energy.",
|
|
132
|
+
lowContext: "Lower vitamin D is very common and may contribute to fatigue, bone health concerns, and immune function.",
|
|
133
|
+
highContext: "Very high vitamin D levels are uncommon and usually only seen with supplementation.",
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
key: "vitamin-b12",
|
|
137
|
+
displayName: "Vitamin B12",
|
|
138
|
+
unit: "pg/mL",
|
|
139
|
+
defaultRange: { low: 200, high: 900, unit: "pg/mL" },
|
|
140
|
+
educationalContext: "Vitamin B12 is essential for nerve function and red blood cell production.",
|
|
141
|
+
lowContext: "Lower B12 may contribute to fatigue, numbness/tingling, and cognitive changes.",
|
|
142
|
+
highContext: "Higher B12 is generally not concerning and often seen with supplementation.",
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
key: "folate",
|
|
146
|
+
displayName: "Folate",
|
|
147
|
+
unit: "ng/mL",
|
|
148
|
+
defaultRange: { low: 3.0, high: 20.0, unit: "ng/mL" },
|
|
149
|
+
educationalContext: "Folate is a B vitamin important for cell division and red blood cell formation.",
|
|
150
|
+
lowContext: "Lower folate may contribute to anemia and fatigue.",
|
|
151
|
+
},
|
|
152
|
+
// Metabolic markers
|
|
153
|
+
{
|
|
154
|
+
key: "glucose",
|
|
155
|
+
displayName: "Glucose (Fasting)",
|
|
156
|
+
unit: "mg/dL",
|
|
157
|
+
defaultRange: { low: 70, high: 99, unit: "mg/dL" },
|
|
158
|
+
educationalContext: "Fasting glucose reflects blood sugar levels after not eating for 8-12 hours.",
|
|
159
|
+
lowContext: "Lower fasting glucose may cause symptoms like shakiness or fatigue.",
|
|
160
|
+
highContext: "Higher fasting glucose may indicate insulin resistance or prediabetes, worth discussing with a clinician.",
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
key: "hemoglobin-a1c",
|
|
164
|
+
displayName: "Hemoglobin A1c",
|
|
165
|
+
unit: "%",
|
|
166
|
+
defaultRange: { low: 4.0, high: 5.6, unit: "%" },
|
|
167
|
+
educationalContext: "HbA1c reflects average blood sugar over the past 2-3 months.",
|
|
168
|
+
highContext: "Higher A1c (5.7-6.4%) may indicate prediabetes. Above 6.5% is in the diabetes range.",
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
key: "insulin",
|
|
172
|
+
displayName: "Insulin (Fasting)",
|
|
173
|
+
unit: "uIU/mL",
|
|
174
|
+
defaultRange: { low: 2.6, high: 24.9, unit: "uIU/mL" },
|
|
175
|
+
educationalContext: "Fasting insulin helps assess how hard the pancreas is working to manage blood sugar.",
|
|
176
|
+
highContext: "Higher fasting insulin may indicate insulin resistance, even with normal glucose.",
|
|
177
|
+
},
|
|
178
|
+
// Lipids
|
|
179
|
+
{
|
|
180
|
+
key: "total-cholesterol",
|
|
181
|
+
displayName: "Total Cholesterol",
|
|
182
|
+
unit: "mg/dL",
|
|
183
|
+
defaultRange: { high: 200, unit: "mg/dL" },
|
|
184
|
+
educationalContext: "Total cholesterol is the sum of all cholesterol types in your blood.",
|
|
185
|
+
highContext: "Higher total cholesterol is one factor in cardiovascular risk assessment.",
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
key: "ldl-cholesterol",
|
|
189
|
+
displayName: "LDL Cholesterol",
|
|
190
|
+
unit: "mg/dL",
|
|
191
|
+
defaultRange: { high: 100, unit: "mg/dL" },
|
|
192
|
+
educationalContext: "LDL is often called 'bad' cholesterol because it can contribute to plaque buildup.",
|
|
193
|
+
highContext: "Higher LDL is associated with increased cardiovascular risk.",
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
key: "hdl-cholesterol",
|
|
197
|
+
displayName: "HDL Cholesterol",
|
|
198
|
+
unit: "mg/dL",
|
|
199
|
+
defaultRange: { low: 40, unit: "mg/dL" },
|
|
200
|
+
maleRange: { low: 40, unit: "mg/dL" },
|
|
201
|
+
femaleRange: { low: 50, unit: "mg/dL" },
|
|
202
|
+
educationalContext: "HDL is often called 'good' cholesterol because it helps remove other cholesterol.",
|
|
203
|
+
lowContext: "Lower HDL may be associated with increased cardiovascular risk.",
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
key: "triglycerides",
|
|
207
|
+
displayName: "Triglycerides",
|
|
208
|
+
unit: "mg/dL",
|
|
209
|
+
defaultRange: { high: 150, unit: "mg/dL" },
|
|
210
|
+
educationalContext: "Triglycerides are a type of fat in the blood, often elevated after meals.",
|
|
211
|
+
highContext: "Higher triglycerides may be influenced by diet, alcohol, or metabolic factors.",
|
|
212
|
+
},
|
|
213
|
+
// Inflammation
|
|
214
|
+
{
|
|
215
|
+
key: "hs-crp",
|
|
216
|
+
displayName: "hs-CRP (High Sensitivity C-Reactive Protein)",
|
|
217
|
+
unit: "mg/L",
|
|
218
|
+
defaultRange: { high: 3.0, unit: "mg/L" },
|
|
219
|
+
educationalContext: "hs-CRP is a marker of inflammation in the body.",
|
|
220
|
+
highContext: "Higher hs-CRP may indicate inflammation, which has many possible causes.",
|
|
221
|
+
},
|
|
222
|
+
// Hormones
|
|
223
|
+
{
|
|
224
|
+
key: "testosterone-total",
|
|
225
|
+
displayName: "Testosterone, Total",
|
|
226
|
+
unit: "ng/dL",
|
|
227
|
+
maleRange: { low: 264, high: 916, unit: "ng/dL" },
|
|
228
|
+
femaleRange: { low: 8, high: 60, unit: "ng/dL" },
|
|
229
|
+
defaultRange: { low: 8, high: 916, unit: "ng/dL" },
|
|
230
|
+
educationalContext: "Testosterone plays a role in energy, muscle mass, and mood in both men and women.",
|
|
231
|
+
lowContext: "Lower testosterone may contribute to fatigue, reduced libido, and mood changes.",
|
|
232
|
+
highContext: "Higher testosterone may have various causes depending on sex and other factors.",
|
|
233
|
+
},
|
|
234
|
+
// Kidney
|
|
235
|
+
{
|
|
236
|
+
key: "creatinine",
|
|
237
|
+
displayName: "Creatinine",
|
|
238
|
+
unit: "mg/dL",
|
|
239
|
+
maleRange: { low: 0.7, high: 1.3, unit: "mg/dL" },
|
|
240
|
+
femaleRange: { low: 0.6, high: 1.1, unit: "mg/dL" },
|
|
241
|
+
defaultRange: { low: 0.6, high: 1.3, unit: "mg/dL" },
|
|
242
|
+
educationalContext: "Creatinine is a waste product filtered by the kidneys.",
|
|
243
|
+
highContext: "Higher creatinine may indicate the kidneys are working harder to filter waste.",
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
key: "bun",
|
|
247
|
+
displayName: "Blood Urea Nitrogen (BUN)",
|
|
248
|
+
unit: "mg/dL",
|
|
249
|
+
defaultRange: { low: 7, high: 20, unit: "mg/dL" },
|
|
250
|
+
educationalContext: "BUN is a waste product from protein metabolism, filtered by the kidneys.",
|
|
251
|
+
highContext: "Higher BUN may indicate dehydration or reduced kidney function.",
|
|
252
|
+
},
|
|
253
|
+
// Liver
|
|
254
|
+
{
|
|
255
|
+
key: "alt",
|
|
256
|
+
displayName: "ALT (Alanine Aminotransferase)",
|
|
257
|
+
unit: "U/L",
|
|
258
|
+
defaultRange: { low: 7, high: 56, unit: "U/L" },
|
|
259
|
+
educationalContext: "ALT is an enzyme found primarily in the liver.",
|
|
260
|
+
highContext: "Higher ALT may indicate liver cell activity or stress.",
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
key: "ast",
|
|
264
|
+
displayName: "AST (Aspartate Aminotransferase)",
|
|
265
|
+
unit: "U/L",
|
|
266
|
+
defaultRange: { low: 10, high: 40, unit: "U/L" },
|
|
267
|
+
educationalContext: "AST is an enzyme found in the liver, heart, and muscles.",
|
|
268
|
+
highContext: "Higher AST may indicate liver, heart, or muscle cell activity.",
|
|
269
|
+
},
|
|
270
|
+
];
|
|
271
|
+
/**
|
|
272
|
+
* Get reference range for a biomarker, considering sex if provided
|
|
273
|
+
*/
|
|
274
|
+
export function getReferenceRange(biomarkerKey, sex) {
|
|
275
|
+
const biomarker = BIOMARKER_RANGES.find((b) => b.key.toLowerCase() === biomarkerKey.toLowerCase());
|
|
276
|
+
return biomarker;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Get the appropriate range values for a biomarker based on sex
|
|
280
|
+
*/
|
|
281
|
+
export function getApplicableRange(biomarker, sex) {
|
|
282
|
+
if (sex === "male" && biomarker.maleRange) {
|
|
283
|
+
return biomarker.maleRange;
|
|
284
|
+
}
|
|
285
|
+
if (sex === "female" && biomarker.femaleRange) {
|
|
286
|
+
return biomarker.femaleRange;
|
|
287
|
+
}
|
|
288
|
+
return biomarker.defaultRange;
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Check if a value is within reference range
|
|
292
|
+
*/
|
|
293
|
+
export function classifyValue(value, range) {
|
|
294
|
+
if (range.low !== undefined && value < range.low) {
|
|
295
|
+
return "low";
|
|
296
|
+
}
|
|
297
|
+
if (range.high !== undefined && value > range.high) {
|
|
298
|
+
return "high";
|
|
299
|
+
}
|
|
300
|
+
if (range.low === undefined && range.high === undefined) {
|
|
301
|
+
return "unknown";
|
|
302
|
+
}
|
|
303
|
+
return "normal";
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Check for critical (red flag) values
|
|
307
|
+
*/
|
|
308
|
+
export function checkCriticalValue(biomarkerKey, value) {
|
|
309
|
+
const threshold = CRITICAL_THRESHOLDS.find((t) => t.key.toLowerCase() === biomarkerKey.toLowerCase());
|
|
310
|
+
if (!threshold) {
|
|
311
|
+
return { isCritical: false };
|
|
312
|
+
}
|
|
313
|
+
if (threshold.criticalLow !== undefined && value < threshold.criticalLow) {
|
|
314
|
+
return { isCritical: true, message: threshold.message };
|
|
315
|
+
}
|
|
316
|
+
if (threshold.criticalHigh !== undefined && value > threshold.criticalHigh) {
|
|
317
|
+
return { isCritical: true, message: threshold.message };
|
|
318
|
+
}
|
|
319
|
+
return { isCritical: false };
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Format a reference range for display
|
|
323
|
+
*/
|
|
324
|
+
export function formatReferenceRange(range) {
|
|
325
|
+
if (range.low !== undefined && range.high !== undefined) {
|
|
326
|
+
return `${range.low}-${range.high} ${range.unit}`;
|
|
327
|
+
}
|
|
328
|
+
if (range.low !== undefined) {
|
|
329
|
+
return `>= ${range.low} ${range.unit}`;
|
|
330
|
+
}
|
|
331
|
+
if (range.high !== undefined) {
|
|
332
|
+
return `<= ${range.high} ${range.unit}`;
|
|
333
|
+
}
|
|
334
|
+
return range.unit;
|
|
335
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "justlabs-mcp-server",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "MCP server for JustLabs - search lab tests, get pricing,
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "MCP server for JustLabs - search lab tests, get pricing, analyze results with educational context",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Basic tests for analysis modules
|
|
3
|
+
* Run with: npx ts-node src/__tests__/analysis.test.ts
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { classifyValue, checkCriticalValue, getReferenceRange, getApplicableRange } from "../ranges/questRanges.js";
|
|
7
|
+
import { analyzePanelResults } from "../analysis/analyzePanelResults.js";
|
|
8
|
+
import { suggestNextSteps } from "../analysis/suggestNextSteps.js";
|
|
9
|
+
import { generatePatientSummary } from "../analysis/generatePatientSummary.js";
|
|
10
|
+
|
|
11
|
+
let passed = 0;
|
|
12
|
+
let failed = 0;
|
|
13
|
+
|
|
14
|
+
function test(name: string, fn: () => void) {
|
|
15
|
+
try {
|
|
16
|
+
fn();
|
|
17
|
+
console.log(`✓ ${name}`);
|
|
18
|
+
passed++;
|
|
19
|
+
} catch (e: any) {
|
|
20
|
+
console.log(`✗ ${name}`);
|
|
21
|
+
console.log(` Error: ${e.message}`);
|
|
22
|
+
failed++;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function expect(actual: any) {
|
|
27
|
+
return {
|
|
28
|
+
toBe(expected: any) {
|
|
29
|
+
if (actual !== expected) {
|
|
30
|
+
throw new Error(`Expected ${expected}, got ${actual}`);
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
toBeTruthy() {
|
|
34
|
+
if (!actual) {
|
|
35
|
+
throw new Error(`Expected truthy value, got ${actual}`);
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
toBeFalsy() {
|
|
39
|
+
if (actual) {
|
|
40
|
+
throw new Error(`Expected falsy value, got ${actual}`);
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
toContain(expected: string) {
|
|
44
|
+
if (typeof actual !== "string" || !actual.includes(expected)) {
|
|
45
|
+
throw new Error(`Expected "${actual}" to contain "${expected}"`);
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
console.log("\n=== Range Classification Tests ===\n");
|
|
52
|
+
|
|
53
|
+
test("classifies value below range as low", () => {
|
|
54
|
+
const result = classifyValue(0.3, { low: 0.45, high: 4.5, unit: "mIU/L" });
|
|
55
|
+
expect(result).toBe("low");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("classifies value above range as high", () => {
|
|
59
|
+
const result = classifyValue(5.0, { low: 0.45, high: 4.5, unit: "mIU/L" });
|
|
60
|
+
expect(result).toBe("high");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("classifies value within range as normal", () => {
|
|
64
|
+
const result = classifyValue(2.5, { low: 0.45, high: 4.5, unit: "mIU/L" });
|
|
65
|
+
expect(result).toBe("normal");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("classifies value with only upper bound", () => {
|
|
69
|
+
const result = classifyValue(50, { high: 34, unit: "IU/mL" });
|
|
70
|
+
expect(result).toBe("high");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
console.log("\n=== Critical Value Detection Tests ===\n");
|
|
74
|
+
|
|
75
|
+
test("detects critical high glucose", () => {
|
|
76
|
+
const result = checkCriticalValue("glucose", 450);
|
|
77
|
+
expect(result.isCritical).toBeTruthy();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("does not flag normal glucose", () => {
|
|
81
|
+
const result = checkCriticalValue("glucose", 95);
|
|
82
|
+
expect(result.isCritical).toBeFalsy();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("detects critical low potassium", () => {
|
|
86
|
+
const result = checkCriticalValue("potassium", 2.3);
|
|
87
|
+
expect(result.isCritical).toBeTruthy();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("detects critical high TSH", () => {
|
|
91
|
+
const result = checkCriticalValue("tsh", 60);
|
|
92
|
+
expect(result.isCritical).toBeTruthy();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
console.log("\n=== Sex-Specific Range Tests ===\n");
|
|
96
|
+
|
|
97
|
+
test("returns male-specific ferritin range", () => {
|
|
98
|
+
const biomarker = getReferenceRange("ferritin");
|
|
99
|
+
expect(biomarker).toBeTruthy();
|
|
100
|
+
const range = getApplicableRange(biomarker!, "male");
|
|
101
|
+
expect(range.high).toBe(400);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("returns female-specific ferritin range", () => {
|
|
105
|
+
const biomarker = getReferenceRange("ferritin");
|
|
106
|
+
const range = getApplicableRange(biomarker!, "female");
|
|
107
|
+
expect(range.high).toBe(200);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
console.log("\n=== Panel Analysis Tests ===\n");
|
|
111
|
+
|
|
112
|
+
test("analyzes fatigue panel with normal results", () => {
|
|
113
|
+
const analysis = analyzePanelResults({
|
|
114
|
+
panel_id: "fatigue_panel",
|
|
115
|
+
results: [
|
|
116
|
+
{ biomarker_key: "tsh", value: 2.0, unit: "mIU/L" },
|
|
117
|
+
{ biomarker_key: "ferritin", value: 80, unit: "ng/mL" },
|
|
118
|
+
{ biomarker_key: "vitamin-b12", value: 500, unit: "pg/mL" },
|
|
119
|
+
{ biomarker_key: "vitamin-d", value: 45, unit: "ng/mL" },
|
|
120
|
+
{ biomarker_key: "hemoglobin", value: 14, unit: "g/dL" },
|
|
121
|
+
{ biomarker_key: "wbc", value: 7.0, unit: "K/uL" },
|
|
122
|
+
],
|
|
123
|
+
});
|
|
124
|
+
expect(analysis.summary.overall_status).toBe("all_normal");
|
|
125
|
+
expect(analysis.red_flags_detected).toBeFalsy();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("detects abnormal TSH in fatigue panel", () => {
|
|
129
|
+
const analysis = analyzePanelResults({
|
|
130
|
+
panel_id: "fatigue_panel",
|
|
131
|
+
results: [
|
|
132
|
+
{ biomarker_key: "tsh", value: 8.0, unit: "mIU/L" },
|
|
133
|
+
{ biomarker_key: "ferritin", value: 80, unit: "ng/mL" },
|
|
134
|
+
{ biomarker_key: "vitamin-b12", value: 500, unit: "pg/mL" },
|
|
135
|
+
{ biomarker_key: "vitamin-d", value: 45, unit: "ng/mL" },
|
|
136
|
+
{ biomarker_key: "hemoglobin", value: 14, unit: "g/dL" },
|
|
137
|
+
{ biomarker_key: "wbc", value: 7.0, unit: "K/uL" },
|
|
138
|
+
],
|
|
139
|
+
});
|
|
140
|
+
expect(analysis.summary.overall_status).toBe("some_outside_range");
|
|
141
|
+
expect(analysis.key_findings.length).toBe(1);
|
|
142
|
+
expect(analysis.key_findings[0].biomarker_key).toBe("tsh");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("stops analysis on critical value", () => {
|
|
146
|
+
const analysis = analyzePanelResults({
|
|
147
|
+
panel_id: "fatigue_panel",
|
|
148
|
+
results: [
|
|
149
|
+
{ biomarker_key: "tsh", value: 60, unit: "mIU/L" }, // Critical high
|
|
150
|
+
{ biomarker_key: "ferritin", value: 80, unit: "ng/mL" },
|
|
151
|
+
],
|
|
152
|
+
});
|
|
153
|
+
expect(analysis.red_flags_detected).toBeTruthy();
|
|
154
|
+
expect(analysis.summary.overall_status).toBe("critical");
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
console.log("\n=== Next Steps Suggestion Tests ===\n");
|
|
158
|
+
|
|
159
|
+
test("suggests thyroid deep dive for abnormal TSH", () => {
|
|
160
|
+
const analysis = analyzePanelResults({
|
|
161
|
+
panel_id: "fatigue_panel",
|
|
162
|
+
results: [
|
|
163
|
+
{ biomarker_key: "tsh", value: 8.0, unit: "mIU/L" },
|
|
164
|
+
{ biomarker_key: "ferritin", value: 80, unit: "ng/mL" },
|
|
165
|
+
{ biomarker_key: "vitamin-b12", value: 500, unit: "pg/mL" },
|
|
166
|
+
{ biomarker_key: "vitamin-d", value: 45, unit: "ng/mL" },
|
|
167
|
+
{ biomarker_key: "hemoglobin", value: 14, unit: "g/dL" },
|
|
168
|
+
{ biomarker_key: "wbc", value: 7.0, unit: "K/uL" },
|
|
169
|
+
],
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const nextSteps = suggestNextSteps("fatigue_panel", analysis);
|
|
173
|
+
expect(nextSteps.next_step.type).toBe("additional_testing");
|
|
174
|
+
if (nextSteps.next_step.type === "additional_testing") {
|
|
175
|
+
expect(nextSteps.next_step.recommended_panel_id).toBe("thyroid_deep_dive");
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("suggests clinician discussion for all normal results", () => {
|
|
180
|
+
const analysis = analyzePanelResults({
|
|
181
|
+
panel_id: "fatigue_panel",
|
|
182
|
+
results: [
|
|
183
|
+
{ biomarker_key: "tsh", value: 2.0, unit: "mIU/L" },
|
|
184
|
+
{ biomarker_key: "ferritin", value: 80, unit: "ng/mL" },
|
|
185
|
+
{ biomarker_key: "vitamin-b12", value: 500, unit: "pg/mL" },
|
|
186
|
+
{ biomarker_key: "vitamin-d", value: 45, unit: "ng/mL" },
|
|
187
|
+
{ biomarker_key: "hemoglobin", value: 14, unit: "g/dL" },
|
|
188
|
+
{ biomarker_key: "wbc", value: 7.0, unit: "K/uL" },
|
|
189
|
+
],
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
const nextSteps = suggestNextSteps("fatigue_panel", analysis);
|
|
193
|
+
expect(nextSteps.next_step.type).toBe("clinician_discussion");
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
console.log("\n=== Patient Summary Tests ===\n");
|
|
197
|
+
|
|
198
|
+
test("generates summary with disclaimer", () => {
|
|
199
|
+
const analysis = analyzePanelResults({
|
|
200
|
+
panel_id: "fatigue_panel",
|
|
201
|
+
results: [
|
|
202
|
+
{ biomarker_key: "tsh", value: 2.0, unit: "mIU/L" },
|
|
203
|
+
{ biomarker_key: "ferritin", value: 80, unit: "ng/mL" },
|
|
204
|
+
],
|
|
205
|
+
});
|
|
206
|
+
const nextSteps = suggestNextSteps("fatigue_panel", analysis);
|
|
207
|
+
const summary = generatePatientSummary("fatigue_panel", analysis, nextSteps);
|
|
208
|
+
|
|
209
|
+
expect(summary.patient_summary.disclaimer).toContain("educational purposes only");
|
|
210
|
+
expect(summary.patient_summary.requires_acknowledgment).toBeTruthy();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("includes CTA only for additional testing", () => {
|
|
214
|
+
const analysis = analyzePanelResults({
|
|
215
|
+
panel_id: "fatigue_panel",
|
|
216
|
+
results: [
|
|
217
|
+
{ biomarker_key: "tsh", value: 8.0, unit: "mIU/L" }, // Abnormal
|
|
218
|
+
{ biomarker_key: "ferritin", value: 80, unit: "ng/mL" },
|
|
219
|
+
],
|
|
220
|
+
});
|
|
221
|
+
const nextSteps = suggestNextSteps("fatigue_panel", analysis);
|
|
222
|
+
const summary = generatePatientSummary("fatigue_panel", analysis, nextSteps);
|
|
223
|
+
|
|
224
|
+
expect(summary.patient_summary.cta).toBeTruthy();
|
|
225
|
+
expect(summary.patient_summary.cta?.url).toContain("justlabs.health");
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// Summary
|
|
229
|
+
console.log("\n=================================");
|
|
230
|
+
console.log(`Tests: ${passed} passed, ${failed} failed`);
|
|
231
|
+
console.log("=================================\n");
|
|
232
|
+
|
|
233
|
+
if (failed > 0) {
|
|
234
|
+
process.exit(1);
|
|
235
|
+
}
|