@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/factors-edit.ts
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import type { Client, ClientFactors, DiseaseEntry, TreatmentItem } from "./types";
|
|
2
|
+
import { endOfMonth } from "./dates";
|
|
3
|
+
|
|
4
|
+
// Pure normalization shared by every editing surface a host offers — a CLI and a web editor
|
|
5
|
+
// alike — so a value entered either way is stored identically.
|
|
6
|
+
|
|
7
|
+
export function capFirst(s: string): string {
|
|
8
|
+
const trimmed = s.trim();
|
|
9
|
+
if (trimmed.length === 0) return trimmed;
|
|
10
|
+
return trimmed.charAt(0).toUpperCase() + trimmed.slice(1);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Disease mutations — pure, so the report-fold (report-merge.ts) and any browser or server-side
|
|
14
|
+
// ingest path can reuse them without depending on a Node CLI.
|
|
15
|
+
// Callers add a disease by content only — id is always minted here, pinned always starts false.
|
|
16
|
+
export function addDisease(client: Client, item: Omit<DiseaseEntry, "id" | "pinned">): void {
|
|
17
|
+
client.factors ??= {};
|
|
18
|
+
client.factors.diseases ??= [];
|
|
19
|
+
const entry: DiseaseEntry = {
|
|
20
|
+
id: crypto.randomUUID(),
|
|
21
|
+
date: endOfMonth(item.date.trim()),
|
|
22
|
+
diagnostic: capFirst(item.diagnostic),
|
|
23
|
+
};
|
|
24
|
+
if (item.summary) entry.summary = item.summary.trim();
|
|
25
|
+
if (item.sourceId) entry.sourceId = item.sourceId;
|
|
26
|
+
const codes = (item.icdCodes ?? []).map((c) => c.trim()).filter(Boolean);
|
|
27
|
+
if (codes.length) entry.icdCodes = codes;
|
|
28
|
+
client.factors.diseases.push(entry);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function removeDiseasesBySourceId(client: Client, sourceId: string): void {
|
|
32
|
+
if (!client.factors?.diseases) return;
|
|
33
|
+
client.factors.diseases = client.factors.diseases.filter((d) => d.sourceId !== sourceId);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function removeDisease(client: Client, diagnostic: string): void {
|
|
37
|
+
if (!client.factors?.diseases) return;
|
|
38
|
+
const v = capFirst(diagnostic);
|
|
39
|
+
client.factors.diseases = client.factors.diseases.filter((d) => d.diagnostic !== v);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function clearDiseases(client: Client): void {
|
|
43
|
+
if (client.factors) client.factors.diseases = [];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const PREGNANCY_VALUES = ["none", "pregnant", "postpartum", "menopause"] as const;
|
|
47
|
+
export const ATHLETIC_VALUES = ["sedentary", "moderate", "endurance"] as const;
|
|
48
|
+
export const SMOKING_VALUES = ["never", "former", "current"] as const;
|
|
49
|
+
|
|
50
|
+
function dedup(xs: string[]): string[] {
|
|
51
|
+
const seen = new Set<string>();
|
|
52
|
+
const out: string[] = [];
|
|
53
|
+
for (const x of xs) {
|
|
54
|
+
if (!seen.has(x)) { seen.add(x); out.push(x); }
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Apply the same normalization the CLI add* verbs apply, then drop empty rows.
|
|
60
|
+
// Operates on a copy — does not mutate the input. Leaves results/finding/ranges
|
|
61
|
+
// and any non-authored fields untouched.
|
|
62
|
+
export function normalizeClientDraft(client: Client): Client {
|
|
63
|
+
const f: ClientFactors = client.factors ? { ...client.factors } : {};
|
|
64
|
+
|
|
65
|
+
if (f.diseases) {
|
|
66
|
+
f.diseases = f.diseases
|
|
67
|
+
.filter((d) => d.diagnostic.trim())
|
|
68
|
+
.map((d) => {
|
|
69
|
+
const entry = { ...d, date: endOfMonth(d.date.trim()), diagnostic: capFirst(d.diagnostic) };
|
|
70
|
+
if (entry.summary != null) {
|
|
71
|
+
const s = entry.summary.trim();
|
|
72
|
+
if (s) entry.summary = s; else delete entry.summary;
|
|
73
|
+
}
|
|
74
|
+
const codes = (entry.icdCodes ?? []).map((c) => c.trim()).filter(Boolean);
|
|
75
|
+
if (codes.length) entry.icdCodes = codes; else delete entry.icdCodes;
|
|
76
|
+
return entry;
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
if (f.treatments) {
|
|
80
|
+
f.treatments = f.treatments
|
|
81
|
+
.filter((t) => t.name.trim())
|
|
82
|
+
.map((t) => {
|
|
83
|
+
const entry: TreatmentItem = { ...t, name: capFirst(t.name), start: endOfMonth((t.start ?? "").trim()) };
|
|
84
|
+
const dose = (t.dose ?? "").trim();
|
|
85
|
+
if (dose) entry.dose = dose; else delete entry.dose;
|
|
86
|
+
if (t.kind) entry.kind = t.kind;
|
|
87
|
+
const end = (t.end ?? "").trim();
|
|
88
|
+
if (end) entry.end = endOfMonth(end); else delete entry.end;
|
|
89
|
+
return entry;
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
if (f.allergies) {
|
|
93
|
+
f.allergies = f.allergies
|
|
94
|
+
.filter((a) => a.allergen.trim())
|
|
95
|
+
.map((a) => {
|
|
96
|
+
const entry = { ...a, allergen: capFirst(a.allergen), reaction: capFirst(a.reaction) };
|
|
97
|
+
if (entry.dateNoted != null) {
|
|
98
|
+
const d = entry.dateNoted.trim();
|
|
99
|
+
if (d) entry.dateNoted = d; else delete entry.dateNoted;
|
|
100
|
+
}
|
|
101
|
+
return entry;
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
if (f.familyHistory) {
|
|
105
|
+
f.familyHistory = f.familyHistory
|
|
106
|
+
.filter((h) => h.relation.trim() && h.condition.trim())
|
|
107
|
+
.map((h) => ({ ...h, relation: capFirst(h.relation), condition: capFirst(h.condition) }));
|
|
108
|
+
}
|
|
109
|
+
if (f.decisions) {
|
|
110
|
+
const out: ClientFactors["decisions"] = [];
|
|
111
|
+
const byIntervention = new Map<string, number>();
|
|
112
|
+
for (const d of f.decisions) {
|
|
113
|
+
if (!d.intervention.trim()) continue;
|
|
114
|
+
const entry = { ...d, intervention: capFirst(d.intervention), purpose: capFirst(d.purpose) };
|
|
115
|
+
const at = byIntervention.get(entry.intervention);
|
|
116
|
+
if (at != null) out![at] = entry;
|
|
117
|
+
else { byIntervention.set(entry.intervention, out!.length); out!.push(entry); }
|
|
118
|
+
}
|
|
119
|
+
f.decisions = out;
|
|
120
|
+
}
|
|
121
|
+
if (f.noteEntries) {
|
|
122
|
+
f.noteEntries = f.noteEntries.filter((n) => n.text.trim()).map((n) => ({ ...n, text: capFirst(n.text) }));
|
|
123
|
+
}
|
|
124
|
+
for (const key of ["ethnicity", "goal", "focus"] as const) {
|
|
125
|
+
if (f[key] != null) {
|
|
126
|
+
const v = capFirst(f[key]!);
|
|
127
|
+
if (v) f[key] = v; else delete f[key];
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (f.height != null) {
|
|
131
|
+
const h = f.height.trim();
|
|
132
|
+
if (h) f.height = h; else delete f.height;
|
|
133
|
+
}
|
|
134
|
+
// Drop emptied arrays back to absent, so saving with nothing entered
|
|
135
|
+
// reproduces the original canonical form (no spurious hash change).
|
|
136
|
+
for (const key of ["diseases", "treatments", "allergies", "familyHistory", "decisions", "noteEntries"] as const) {
|
|
137
|
+
if (f[key] && f[key]!.length === 0) delete f[key];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
let study = client.study ? { ...client.study } : undefined;
|
|
141
|
+
if (study) {
|
|
142
|
+
if (study.entries) {
|
|
143
|
+
study.entries = study.entries
|
|
144
|
+
.filter((e) => e.focus.trim())
|
|
145
|
+
.map((e) => ({ ...e, focus: e.focus.trim(), detail: e.detail.trim() }));
|
|
146
|
+
if (study.entries.length === 0) delete study.entries;
|
|
147
|
+
}
|
|
148
|
+
if (!study.entries) study = undefined;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const recommended = client.recommended
|
|
152
|
+
? dedup(client.recommended.map((w) => w.trim()).filter(Boolean))
|
|
153
|
+
: client.recommended;
|
|
154
|
+
return {
|
|
155
|
+
...client,
|
|
156
|
+
factors: f,
|
|
157
|
+
study,
|
|
158
|
+
watchlist: dedup(client.watchlist.map((w) => w.trim()).filter(Boolean)),
|
|
159
|
+
recommended: recommended && recommended.length === 0 ? undefined : recommended,
|
|
160
|
+
};
|
|
161
|
+
}
|