@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.
@@ -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
+ }