@pablotech/akesi 0.1.22
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/LICENSE +21 -0
- package/README.md +432 -0
- package/benchmarks/retry-corrections.ts +231 -0
- package/dates.ts +36 -0
- package/document-model.ts +96 -0
- package/document-read.ts +171 -0
- package/factors-edit.ts +161 -0
- package/finding-assemble.ts +803 -0
- package/finding-generate.ts +1439 -0
- package/finding-regroup.ts +167 -0
- package/imaging-catalog.ts +108 -0
- package/index.ts +4 -0
- package/ingest-core.ts +115 -0
- package/item-registry.ts +69 -0
- package/marker-deltas.ts +84 -0
- package/marker-groups-prompt.ts +198 -0
- package/package.json +69 -0
- package/parsers-report.ts +21 -0
- package/pinned-queries.ts +113 -0
- package/ranges-prompt.ts +228 -0
- package/ranges.ts +94 -0
- package/report-extract.ts +337 -0
- package/report-merge.ts +372 -0
- package/report-title.ts +29 -0
- package/section-labels.ts +20 -0
- package/system-groups.ts +43 -0
- package/treatment-bucket.ts +366 -0
- package/treatment-infer.ts +237 -0
- package/treatment-normalize.ts +91 -0
- package/treatment-product.ts +111 -0
- package/treatment-timing-rules.ts +90 -0
- package/types.ts +647 -0
- package/unit-systems.ts +214 -0
- package/vitest.config.ts +8 -0
package/unit-systems.ts
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
// Whole-system unit conversion (US-conventional ↔ SI / metric), per reading.
|
|
2
|
+
//
|
|
3
|
+
// The selector picks a measurement SYSTEM, not just lb/kg: body metrics (lb/in ↔ kg/cm)
|
|
4
|
+
// AND lab analytes (mg/dL ↔ mmol/L, ng/mL ↔ nmol/L). Conversion is analyte-specific, so
|
|
5
|
+
// the molar table is keyed by marker NAME (the stable identity — there is no marker id).
|
|
6
|
+
//
|
|
7
|
+
// Design rules:
|
|
8
|
+
// - Stored values stay AS-REPORTED (faithful to source). All conversion is on read.
|
|
9
|
+
// - ANY marker may arrive in ANY unit, and one series may MIX units across dates. So
|
|
10
|
+
// conversion is PER READING from that reading's own unit, and a mixed-unit series is
|
|
11
|
+
// reconciled to a canonical (SI) unit before it is charted/aggregated (normalizeSeries).
|
|
12
|
+
// - CLINICAL SAFETY: only analytes with a VERIFIED factor are converted; anything else
|
|
13
|
+
// passes through in its stored unit, untouched, and is surfaced by `unmappedConvertible`
|
|
14
|
+
// + the coverage-gate test. A missing conversion is safe; a wrong one is a clinical error.
|
|
15
|
+
|
|
16
|
+
export type UnitSystem = "metric" | "imperial"; // metric = SI, imperial = US-conventional
|
|
17
|
+
|
|
18
|
+
// ── Physical units (body metrics). Canonical = the metric base unit. ──────────────────
|
|
19
|
+
// metric unit → imperial display (factor: metric × factor = imperial).
|
|
20
|
+
export const PHYSICAL_TO_IMPERIAL: Record<string, { unit: string; factor: number }> = {
|
|
21
|
+
kg: { unit: "lb", factor: 2.20462 },
|
|
22
|
+
g: { unit: "lb", factor: 0.00220462 },
|
|
23
|
+
cm: { unit: "in", factor: 0.393701 },
|
|
24
|
+
"cm²": { unit: "in²", factor: 0.155 },
|
|
25
|
+
"cm³": { unit: "in³", factor: 0.0610237 },
|
|
26
|
+
mm: { unit: "in", factor: 0.0393701 },
|
|
27
|
+
// Dimensionally exact (1 g/L = 100 mg/dL, no molar mass) → safe for ANY marker stored in
|
|
28
|
+
// g/L (e.g. serum globulins); the SI side is g/L, the US/conventional side mg/dL.
|
|
29
|
+
"g/L": { unit: "mg/dL", factor: 100 },
|
|
30
|
+
};
|
|
31
|
+
// imperial unit → metric canonical (for the rare case raw data arrives imperial).
|
|
32
|
+
const IMPERIAL_TO_METRIC: Record<string, { unit: string; factor: number }> = {
|
|
33
|
+
lb: { unit: "kg", factor: 0.453592 },
|
|
34
|
+
in: { unit: "cm", factor: 2.54 },
|
|
35
|
+
"in²": { unit: "cm²", factor: 6.4516 },
|
|
36
|
+
"in³": { unit: "cm³", factor: 16.387064 },
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// M93 — markers where the metric unit IS the US-prevailing clinical convention, so the
|
|
40
|
+
// generic PHYSICAL_TO_IMPERIAL unit-keyed table must NOT convert them (e.g. visceral adipose
|
|
41
|
+
// tissue mass is reported in grams in US practice too, never ounces/lb — the owner's example,
|
|
42
|
+
// extended here to the sibling DEXA body-composition markers sharing the same convention).
|
|
43
|
+
// NEEDS VERIFICATION against a body-composition/DEXA clinical reference before relying on this
|
|
44
|
+
// beyond the owner-confirmed VAT case.
|
|
45
|
+
export const PHYSICAL_NO_CONVERT = new Set<string>([
|
|
46
|
+
"Visceral adipose tissue mass",
|
|
47
|
+
"Visceral adipose tissue area",
|
|
48
|
+
"Visceral adipose tissue volume",
|
|
49
|
+
"Subcutaneous adipose tissue area",
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
// M93 — cosmetic unit-STRING variants for the same physical unit, straight from different lab
|
|
53
|
+
// source formats (values stay as-reported per the module doc above; only the DISPLAYED label is
|
|
54
|
+
// canonicalized, never the stored data). eGFR renders identically regardless of which lab
|
|
55
|
+
// formatted its unit string.
|
|
56
|
+
const UNIT_ALIAS: Record<string, string> = {
|
|
57
|
+
"mL/min /1.73m2": "mL/min/1.73m²",
|
|
58
|
+
"mL/min per 1.73 m2": "mL/min/1.73m²",
|
|
59
|
+
};
|
|
60
|
+
export function canonicalUnit(unit: string): string {
|
|
61
|
+
return UNIT_ALIAS[unit] ?? unit;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// M93 — compound clinical units that are, on research, reported identically in US and SI/
|
|
65
|
+
// international practice — no US-customary form exists in routine use, so no conversion ever
|
|
66
|
+
// applies regardless of marker. NEEDS VERIFICATION against a clinical reference before treating
|
|
67
|
+
// as final; each entry's marker(s) are noted so a reviewer can check the specific convention.
|
|
68
|
+
export const VERIFIED_NO_CONVERT_UNITS = new Set<string>([
|
|
69
|
+
"g/cm²", // DEXA bone mineral density (BMD)
|
|
70
|
+
"kg/m²", // BMI-family indices (BMI, FMI, LMI, ALMI)
|
|
71
|
+
"g/m²", // BMC/height²
|
|
72
|
+
"cm²/m²", // aortic valve area index
|
|
73
|
+
"ml/m²", // cardiac chamber volume indices (LA/RA volume index, LVOT stroke-volume index)
|
|
74
|
+
"nmol/min/mL", // Lp-PLA2 activity (assay-specific, not a US/SI convention)
|
|
75
|
+
"mL/min/1.73m²", // eGFR (canonical, post-alias)
|
|
76
|
+
]);
|
|
77
|
+
|
|
78
|
+
// ── Analyte molar units. Canonical = the SI side. `k` is conventional→SI: si = us × k. ──
|
|
79
|
+
// Each factor is a published clinical conversion; molar mass noted for audit.
|
|
80
|
+
export interface AnalyteRule {
|
|
81
|
+
us: string; // US-conventional unit
|
|
82
|
+
si: string; // SI unit (canonical)
|
|
83
|
+
k: number; // us_value × k = si_value
|
|
84
|
+
}
|
|
85
|
+
export const ANALYTE: Record<string, AnalyteRule> = {
|
|
86
|
+
// Lipid/glucose panel — mirrors the in-repo ingest table (parsers/healthmatters.ts
|
|
87
|
+
// NORMALIZE, in production since W1); 1/k equals that table's factor (cross-checked by test).
|
|
88
|
+
Glucose: { us: "mg/dL", si: "mmol/L", k: 1 / 18.02 }, // glucose MW 180.16
|
|
89
|
+
"Estimated Average Glucose (eAG)": { us: "mg/dL", si: "mmol/L", k: 1 / 18.02 },
|
|
90
|
+
"Total Cholesterol": { us: "mg/dL", si: "mmol/L", k: 1 / 38.67 }, // chol MW 386.65
|
|
91
|
+
"HDL-C": { us: "mg/dL", si: "mmol/L", k: 1 / 38.67 },
|
|
92
|
+
"LDL-C": { us: "mg/dL", si: "mmol/L", k: 1 / 38.67 },
|
|
93
|
+
"Non-HDL Cholesterol": { us: "mg/dL", si: "mmol/L", k: 1 / 38.67 },
|
|
94
|
+
"VLDL Cholesterol Cal": { us: "mg/dL", si: "mmol/L", k: 1 / 38.67 },
|
|
95
|
+
Triglycerides: { us: "mg/dL", si: "mmol/L", k: 1 / 88.57 }, // triolein MW ~885
|
|
96
|
+
"Apolipoprotein B": { us: "mg/dL", si: "g/L", k: 0.01 },
|
|
97
|
+
"Apolipoprotein A-1": { us: "mg/dL", si: "g/L", k: 0.01 },
|
|
98
|
+
// Core metabolic panel — standard factors, corroborated against clinical SI tables.
|
|
99
|
+
"Creatinine, Serum": { us: "mg/dL", si: "µmol/L", k: 88.4 }, // creatinine MW 113.12
|
|
100
|
+
"Creatinine, Random Urine": { us: "mg/dL", si: "µmol/L", k: 88.4 },
|
|
101
|
+
"Calcium, Serum": { us: "mg/dL", si: "mmol/L", k: 0.2495 }, // Ca MW 40.08 (1/4.008)
|
|
102
|
+
"Adjusted Calcium": { us: "mg/dL", si: "mmol/L", k: 0.2495 },
|
|
103
|
+
"Uric Acid": { us: "mg/dL", si: "µmol/L", k: 59.48 }, // urate MW 168.11
|
|
104
|
+
"Bilirubin Total": { us: "mg/dL", si: "µmol/L", k: 17.1 }, // bilirubin MW 584.66
|
|
105
|
+
"Bilirubin Direct": { us: "mg/dL", si: "µmol/L", k: 17.1 },
|
|
106
|
+
"Bilirubin Indirect": { us: "mg/dL", si: "µmol/L", k: 17.1 },
|
|
107
|
+
"Vitamin D, 25-Hydroxy": { us: "ng/mL", si: "nmol/L", k: 2.496 }, // calcidiol MW 400.64
|
|
108
|
+
// Endocrine, metabolic, vitamins/minerals — factors from the GlobalRPH conventional↔SI
|
|
109
|
+
// table (a standard clinical reference; cross-checked against the search results). `us`
|
|
110
|
+
// matches the EXACT stored unit string ("µg/dL" micro-sign vs "ug/dL" ASCII differ).
|
|
111
|
+
Testosterone: { us: "ng/dL", si: "nmol/L", k: 0.0347 }, // testosterone MW 288.4
|
|
112
|
+
"Testosterone, bioavailable (male)": { us: "ng/dL", si: "nmol/L", k: 0.0347 }, // same molecule
|
|
113
|
+
"Free testosterone": { us: "pg/mL", si: "pmol/L", k: 3.47 }, // derived from testosterone MW (1000/288.4)
|
|
114
|
+
"Estradiol (male)": { us: "pg/mL", si: "pmol/L", k: 3.671 },
|
|
115
|
+
"Cortisol, Serum": { us: "µg/dL", si: "nmol/L", k: 27.59 },
|
|
116
|
+
"Progesterone (male)": { us: "ng/mL", si: "nmol/L", k: 3.18 },
|
|
117
|
+
"T4, Total (Thyroxine)": { us: "ug/dL", si: "nmol/L", k: 12.87 },
|
|
118
|
+
"T4, Free": { us: "ng/dL", si: "pmol/L", k: 12.87 }, // stored SI (pmol/L)
|
|
119
|
+
"IGF 1, LC/MS": { us: "ng/mL", si: "nmol/L", k: 0.131 },
|
|
120
|
+
"C-Peptide, LC/MS/MS": { us: "ng/mL", si: "nmol/L", k: 0.333 },
|
|
121
|
+
"Vitamin B12": { us: "pg/mL", si: "pmol/L", k: 0.738 },
|
|
122
|
+
"Vitamin B9 (Folate)": { us: "ng/mL", si: "nmol/L", k: 2.266 },
|
|
123
|
+
"Folate, RBC": { us: "ng/mL", si: "nmol/L", k: 2.266 },
|
|
124
|
+
Iron: { us: "µg/dL", si: "µmol/L", k: 0.179 },
|
|
125
|
+
"Total iron-binding capacity (TIBC)": { us: "ug/dL", si: "µmol/L", k: 0.179 },
|
|
126
|
+
"UIBC Blood Test (Unsaturated Iron Binding Capacity)": { us: "µg/dL", si: "µmol/L", k: 0.179 },
|
|
127
|
+
"Copper, Serum or Plasma": { us: "ug/dL", si: "µmol/L", k: 0.157 },
|
|
128
|
+
Zinc: { us: "µg/dL", si: "µmol/L", k: 0.153 },
|
|
129
|
+
Magnesium: { us: "mg/dL", si: "mmol/L", k: 0.411 }, // stored SI (mmol/L)
|
|
130
|
+
"Magnesium, RBC": { us: "mg/dL", si: "mmol/L", k: 0.411 },
|
|
131
|
+
"Phosphate (Phosphorus)": { us: "mg/dL", si: "mmol/L", k: 0.323 }, // stored SI (mmol/L)
|
|
132
|
+
"Blood urea nitrogen (BUN)": { us: "mg/dL", si: "mmol/L", k: 0.357 }, // BUN → urea (SI)
|
|
133
|
+
// Monovalent electrolytes: mEq/L (US) and mmol/L (SI) are numerically identical (valence 1).
|
|
134
|
+
"Potassium, Serum (Kalium)": { us: "mEq/L", si: "mmol/L", k: 1 },
|
|
135
|
+
"Sodium, Serum (Natrium)": { us: "mEq/L", si: "mmol/L", k: 1 },
|
|
136
|
+
// M93 — urine albumin/creatinine ratio: US commonly reports mg/g creatinine; UK/Canada/
|
|
137
|
+
// Australia and KDIGO's international staging table use mg/mmol creatinine. Factor derived
|
|
138
|
+
// from creatinine MW 113.12 g/mol (1 g creatinine = 8.84 mmol, so mg/mmol = mg/g × 0.1131).
|
|
139
|
+
// NEEDS VERIFICATION against a clinical reference before relying on this — flagged per the
|
|
140
|
+
// owner's standing correctness concern, unlike the other factors above which are
|
|
141
|
+
// cross-checked against the in-repo ingest table or a published clinical SI reference.
|
|
142
|
+
"Albumin/Creatinine Ratio, Random Urine": { us: "mg/g creat", si: "mg/mmol creat", k: 0.1131 },
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// The units that genuinely differ US↔SI, or need an explicit verified-non-convertible decision —
|
|
146
|
+
// used by the coverage gate to flag any (marker, unit) pair carrying one of these WITHOUT a rule
|
|
147
|
+
// (so a gap can never be silent). M93 extended this beyond molar-concentration units to the
|
|
148
|
+
// compound/physical units researched above.
|
|
149
|
+
export const CONVERTIBLE_UNIT_CLASS = new Set([
|
|
150
|
+
"mg/dL", "ng/dL", "pg/mL", "µg/dL", "ug/dL", "mcg/dL", "ng/mL",
|
|
151
|
+
"nmol/L", "pmol/L", "µmol/L", "umol/L", "mmol/L", "µg/ml", "ug/ml", "g/L", "mg/L",
|
|
152
|
+
"mg/g creat",
|
|
153
|
+
...VERIFIED_NO_CONVERT_UNITS,
|
|
154
|
+
]);
|
|
155
|
+
|
|
156
|
+
export function recognizes(marker: string, unit: string): boolean {
|
|
157
|
+
const u = canonicalUnit(unit);
|
|
158
|
+
const a = ANALYTE[marker];
|
|
159
|
+
if (a && (u === a.us || u === a.si)) return true;
|
|
160
|
+
if (VERIFIED_NO_CONVERT_UNITS.has(u)) return true;
|
|
161
|
+
return u in PHYSICAL_TO_IMPERIAL || u in IMPERIAL_TO_METRIC;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Fold one reading to its canonical unit (analyte → SI; physical → metric base). An
|
|
165
|
+
// unrecognized unit is returned unchanged (never guessed).
|
|
166
|
+
export function toCanonical(marker: string, unit: string, value: number): { value: number; unit: string } {
|
|
167
|
+
const u = canonicalUnit(unit);
|
|
168
|
+
const a = ANALYTE[marker];
|
|
169
|
+
if (a) {
|
|
170
|
+
if (u === a.si) return { value, unit: a.si };
|
|
171
|
+
if (u === a.us) return { value: value * a.k, unit: a.si };
|
|
172
|
+
return { value, unit: u };
|
|
173
|
+
}
|
|
174
|
+
if (VERIFIED_NO_CONVERT_UNITS.has(u)) return { value, unit: u };
|
|
175
|
+
const imp = IMPERIAL_TO_METRIC[u];
|
|
176
|
+
if (imp) return { value: value * imp.factor, unit: imp.unit };
|
|
177
|
+
return { value, unit: u }; // already metric, or system-invariant, or unknown
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
interface SeriesRow { marker: string; unit: string; value: number; valueText?: string; ref?: { low?: number; high?: number } }
|
|
181
|
+
|
|
182
|
+
// Reconcile a marker's readings to ONE unit. If the series is already single-unit, it is
|
|
183
|
+
// returned untouched (so the Finding/CLI basis is unchanged for today's single-unit data);
|
|
184
|
+
// only a genuinely mixed-unit series (e.g. an xls with both mg/dL and mmol/L) is folded to
|
|
185
|
+
// canonical so it can share one axis/scale and so deltas subtract like-for-like.
|
|
186
|
+
export function normalizeSeries<T extends SeriesRow>(rows: T[]): T[] {
|
|
187
|
+
const units = new Set(rows.filter((r) => !r.valueText).map((r) => r.unit));
|
|
188
|
+
if (units.size <= 1) return rows;
|
|
189
|
+
return rows.map((r) => {
|
|
190
|
+
if (r.valueText) return r;
|
|
191
|
+
const c = toCanonical(r.marker, r.unit, r.value);
|
|
192
|
+
if (c.unit === r.unit) return r;
|
|
193
|
+
const ref = r.ref
|
|
194
|
+
? {
|
|
195
|
+
low: r.ref.low != null ? toCanonical(r.marker, r.unit, r.ref.low).value : undefined,
|
|
196
|
+
high: r.ref.high != null ? toCanonical(r.marker, r.unit, r.ref.high).value : undefined,
|
|
197
|
+
}
|
|
198
|
+
: undefined;
|
|
199
|
+
return { ...r, value: c.value, unit: c.unit, ...(ref ? { ref } : {}) };
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// The convertible-class (marker, unit) pairs present in `rows` that have NO conversion rule.
|
|
204
|
+
// Powers the coverage-gate test and a dev console warning — unmapped analytes render in their
|
|
205
|
+
// stored unit, visibly tracked, never coerced.
|
|
206
|
+
export function unmappedConvertible(rows: { marker: string; unit: string }[]): { marker: string; unit: string }[] {
|
|
207
|
+
const seen = new Map<string, { marker: string; unit: string }>();
|
|
208
|
+
for (const r of rows) {
|
|
209
|
+
if (!CONVERTIBLE_UNIT_CLASS.has(canonicalUnit(r.unit))) continue;
|
|
210
|
+
if (recognizes(r.marker, r.unit)) continue;
|
|
211
|
+
seen.set(`${r.marker}|${r.unit}`, { marker: r.marker, unit: r.unit });
|
|
212
|
+
}
|
|
213
|
+
return [...seen.values()].sort((a, b) => a.marker.localeCompare(b.marker));
|
|
214
|
+
}
|
package/vitest.config.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { defineConfig } from "vitest/config";
|
|
2
|
+
|
|
3
|
+
// The finding/ranges/report-extract prompt builders read the current date and local-timezone
|
|
4
|
+
// getters (patient age, overdue-marker windows). Pinned so a standalone `npm test` here can't
|
|
5
|
+
// disagree with the app's own TZ=UTC-pinned run over the same golden fixtures.
|
|
6
|
+
export default defineConfig({
|
|
7
|
+
test: { env: { TZ: "UTC" } },
|
|
8
|
+
});
|