@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/types.ts
ADDED
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
export interface MarkerResult {
|
|
4
|
+
marker: string;
|
|
5
|
+
group: string;
|
|
6
|
+
source: string;
|
|
7
|
+
date: string;
|
|
8
|
+
value: number;
|
|
9
|
+
unit: string;
|
|
10
|
+
// Display override for semi-quantitative results whose `value` is a stand-in
|
|
11
|
+
// for sorting/charting (antibody titers: value 80 → valueText "1:80"). When
|
|
12
|
+
// present, render this verbatim instead of `value`/`unit`.
|
|
13
|
+
valueText?: string;
|
|
14
|
+
ref?: { low?: number; high?: number };
|
|
15
|
+
// Provenance: id of the source file (SourceRecord.id) this reading came from —
|
|
16
|
+
// a lab xlsx, a DEXA/scale export, or an imaging report. Absent for readings
|
|
17
|
+
// ingested before provenance tracking.
|
|
18
|
+
sourceId?: string;
|
|
19
|
+
// Backfilled from a LATER report's "vs prior" narrative (e.g. the 2026 echo
|
|
20
|
+
// states the 2021 mean gradient was 10), not directly measured. A low-priority
|
|
21
|
+
// placeholder: a directly-measured reading at the same marker|date always
|
|
22
|
+
// replaces it. `sourceId` points at the report that stated the comparison.
|
|
23
|
+
fromComparison?: true;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type InferenceMode = "dev" | "prod";
|
|
27
|
+
|
|
28
|
+
export interface GeneratedBy {
|
|
29
|
+
mode: InferenceMode;
|
|
30
|
+
model: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface PersonalizedRange {
|
|
34
|
+
low?: number;
|
|
35
|
+
high?: number;
|
|
36
|
+
unit: string;
|
|
37
|
+
// Plain-language "what this marker is / what it reflects" — a short definition shown
|
|
38
|
+
// contextually in the marker chart, independent of the range rationale below.
|
|
39
|
+
meaning?: string;
|
|
40
|
+
explanation: string;
|
|
41
|
+
explanationImperial?: string;
|
|
42
|
+
// General population/guideline range based ONLY on age, gender, and height —
|
|
43
|
+
// the baseline the personalized range is shifted from. Same unit/scale as low/high.
|
|
44
|
+
generalLow?: number;
|
|
45
|
+
generalHigh?: number;
|
|
46
|
+
generalExplanation?: string;
|
|
47
|
+
generatedAt: string;
|
|
48
|
+
factorsHash: string;
|
|
49
|
+
generatedBy?: GeneratedBy;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type TreatmentKind = "drug" | "supplement" | "behavior";
|
|
53
|
+
|
|
54
|
+
// One attached photo/document, stored raw at `key` (a host's object store, mirroring
|
|
55
|
+
// PendingUpload.file's "<sha8>-<safeName>" shape) and referenced from whichever leaf item it's
|
|
56
|
+
// attached to (NoteEntry, TreatmentItem, DecisionEntry, StudyEntry, AllergyEntry,
|
|
57
|
+
// FamilyHistoryEntry, DiseaseEntry, ChatTurn).
|
|
58
|
+
export interface Attachment {
|
|
59
|
+
key: string;
|
|
60
|
+
name: string;
|
|
61
|
+
mediaType: string;
|
|
62
|
+
bytes: number;
|
|
63
|
+
addedAt: string;
|
|
64
|
+
// METADATA ONLY about a document whose contents have been read (document-read.ts). The text
|
|
65
|
+
// itself deliberately lives outside the vault, in an R2 sidecar keyed by this attachment's own
|
|
66
|
+
// content-addressed key — a long PDF's transcription would otherwise bloat a blob that is
|
|
67
|
+
// re-encrypted and rewritten on every unrelated edit. This is what the UI shows and what tells a
|
|
68
|
+
// consumer there is a sidecar worth fetching. Absent on an image, and on any document attached
|
|
69
|
+
// before extraction existed.
|
|
70
|
+
extracted?: {
|
|
71
|
+
at: string;
|
|
72
|
+
chars: number;
|
|
73
|
+
/** document-read.ts's own label for what the document is, e.g. "Radiology report". */
|
|
74
|
+
kind?: string;
|
|
75
|
+
/** Set when extraction was attempted and failed, so the UI can say so instead of staying blank. */
|
|
76
|
+
error?: string;
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// One ingredient off a product's own label, with the amount PER SERVING as printed. These are label
|
|
81
|
+
// facts about the product, deliberately kept apart from the patient's own dose entry
|
|
82
|
+
// (doseAmount/doseUnit/doseFrequency below): "100mcg Selenium" is what one capsule contains, not how
|
|
83
|
+
// much the patient takes. Nothing here may ever reach formatDose() or treatmentLabel() — that label
|
|
84
|
+
// is the cross-system matching key for stored Findings, so a label amount leaking into it would
|
|
85
|
+
// silently unmatch every existing reference.
|
|
86
|
+
export interface Ingredient {
|
|
87
|
+
name: string;
|
|
88
|
+
amount?: number;
|
|
89
|
+
unit?: string;
|
|
90
|
+
// The chemical form or source as printed — "L-Selenomethionine", "20% Coleus Forskohlii Extract".
|
|
91
|
+
form?: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// The label's OWN suggested serving/administration — a LABEL FACT like Ingredient, extracted by
|
|
95
|
+
// the extraction pipeline, never typed by the patient. Medicine-scope, fans across dose rows like description/
|
|
96
|
+
// ingredients/links. `unit` uses the same vocabulary as TreatmentItem.doseUnit, because a new
|
|
97
|
+
// record's doseUnit gets locked to this value. `unitsPerServing` exists because Ingredient.amount
|
|
98
|
+
// is per SERVING, and a serving is not always one countable unit ("Serving Size: 2 softgels").
|
|
99
|
+
export interface Administration {
|
|
100
|
+
unit: string;
|
|
101
|
+
unitsPerServing: number;
|
|
102
|
+
suggestedUnits: number;
|
|
103
|
+
suggestedFrequency: DoseFrequency;
|
|
104
|
+
// The package's OWN total count, as printed (e.g. a bottle of 60 capsules) — a different fact
|
|
105
|
+
// from `suggestedUnits` (the per-administration recommended count) and not used in the daily-
|
|
106
|
+
// total arithmetic; captured because it's on the label and otherwise lost. Absent when the
|
|
107
|
+
// source doesn't state a total (a torn label, a partial photo, pasted text with no count).
|
|
108
|
+
containerQuantity?: number;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// An external reference for a product — a third-party COA, a spec sheet. The URL is stored and shown
|
|
112
|
+
// and handed to the model as a citable reference; nothing fetches it (see the milestone's
|
|
113
|
+
// out-of-scope note: an outbound fetch to a user-supplied URL from the PHI-handling Function is an
|
|
114
|
+
// SSRF and prompt-injection surface). `url` is validated http/https at the boundary — see
|
|
115
|
+
// treatment-product.ts.
|
|
116
|
+
export interface ProductLink {
|
|
117
|
+
label: string;
|
|
118
|
+
url: string;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// A single heterogeneous intervention: a drug/supplement (with a dose) or a behavior
|
|
122
|
+
// (no dose). Its temporal bucket (past / ongoing / planned) is DERIVED from start/end
|
|
123
|
+
// (treatment-bucket.ts), never stored. `start`/`end` are ISO `YYYY-MM` (or `YYYY`);
|
|
124
|
+
// an empty `start` means "taking it, date unknown" → ongoing.
|
|
125
|
+
export type DoseFrequency = "day" | "week" | "month" | "as needed";
|
|
126
|
+
|
|
127
|
+
export interface TreatmentItem {
|
|
128
|
+
id: string;
|
|
129
|
+
pinned?: boolean;
|
|
130
|
+
name: string;
|
|
131
|
+
// M104 — superseded by doseAmount/doseUnit/doseFrequency (below) for anything entered through the
|
|
132
|
+
// edit form; kept as the display/matching fallback for un-migrated records (see formatDose() in
|
|
133
|
+
// treatment-bucket.ts, and whatever one-time conversion a host runs).
|
|
134
|
+
dose?: string;
|
|
135
|
+
doseAmount?: number;
|
|
136
|
+
doseUnit?: string;
|
|
137
|
+
doseFrequency?: DoseFrequency;
|
|
138
|
+
kind?: TreatmentKind;
|
|
139
|
+
start: string;
|
|
140
|
+
end?: string;
|
|
141
|
+
reason?: string;
|
|
142
|
+
timingPeriod?: "AM" | "PM";
|
|
143
|
+
// Superseded by `attachments` (below); kept read-only for un-migrated vaults, same
|
|
144
|
+
// shim pattern as LegacyFactors. New writes always use `attachments`; see a host's
|
|
145
|
+
// attachment-store fold function.
|
|
146
|
+
images?: string[];
|
|
147
|
+
attachments?: Attachment[];
|
|
148
|
+
// The PRODUCT, not this dose period — what the thing is, what is in it, where its paperwork lives.
|
|
149
|
+
// Medicine-level like name/reason/kind (and now attachments): a medicine-scope save fans these
|
|
150
|
+
// across every dose row, and the per-entry editor never shows them. For a formulated supplement
|
|
151
|
+
// this is the entire clinical content — "Thyroid Support" names nothing on its own.
|
|
152
|
+
description?: string;
|
|
153
|
+
// The manufacturer/brand as printed — a label fact like description, same medicine-scope fan-out.
|
|
154
|
+
maker?: string;
|
|
155
|
+
ingredients?: Ingredient[];
|
|
156
|
+
links?: ProductLink[];
|
|
157
|
+
// The label's own suggested serving/administration — see Administration's own comment. Same
|
|
158
|
+
// medicine-scope fan-out as description/ingredients/links above.
|
|
159
|
+
administration?: Administration;
|
|
160
|
+
// Provenance: set once, inside identifyFrom()'s success branch (a host-side UI component), when
|
|
161
|
+
// name/kind/description/ingredients/links came from reading a photo or pasted text rather than
|
|
162
|
+
// being typed by hand. One flag for the whole merge — identifyFrom() answers one
|
|
163
|
+
// question, "what is this product," in one call — mirrors DiseaseEntry.sourceId/summary's
|
|
164
|
+
// "absent for hand-entered" convention. Absent for a hand-entered treatment.
|
|
165
|
+
extracted?: { via: "photo" | "text"; at: string };
|
|
166
|
+
// The Attachment key(s) this extraction was read from — a subset of `attachments` (above), so a
|
|
167
|
+
// later, unrelated manual Attach is never mistaken for the original raw capture. Meaningful only
|
|
168
|
+
// when extracted?.via === "photo".
|
|
169
|
+
rawCaptureAttachmentKeys?: string[];
|
|
170
|
+
// The exact text pasted into "From text" that produced this extraction, kept verbatim so the
|
|
171
|
+
// original raw source is always distinguishable from what the extraction pipeline inferred from
|
|
172
|
+
// it. Meaningful only when extracted?.via === "text".
|
|
173
|
+
rawCaptureText?: string;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Pre-unification on-disk shapes, read ONLY by the load-shim (treatment-normalize.ts) to
|
|
177
|
+
// fold an un-migrated vault into `treatments`. Never written.
|
|
178
|
+
export interface LegacyTreatmentItem { drug?: string; dose?: string; since?: string }
|
|
179
|
+
export interface LegacyPlanEntry { action?: string; date?: string }
|
|
180
|
+
export interface LegacyFactors {
|
|
181
|
+
medications?: LegacyTreatmentItem[];
|
|
182
|
+
supplements?: LegacyTreatmentItem[];
|
|
183
|
+
plan?: LegacyPlanEntry[];
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export interface DiseaseEntry {
|
|
187
|
+
id: string;
|
|
188
|
+
pinned?: boolean;
|
|
189
|
+
attachments?: Attachment[];
|
|
190
|
+
date: string;
|
|
191
|
+
diagnostic: string;
|
|
192
|
+
// Up to ~3 sentences capturing the nature/metrics of the finding (e.g. CAC
|
|
193
|
+
// score + per-vessel detail, valve morphology, steatosis grade/extent). Drives
|
|
194
|
+
// the Finding alongside the terse `diagnostic`. Populated for imaging-extracted
|
|
195
|
+
// diagnoses; absent for hand-entered ones.
|
|
196
|
+
summary?: string;
|
|
197
|
+
// Provenance: id of the source file (SourceRecord.id) when extracted from an
|
|
198
|
+
// imported medical report. Absent for hand-entered diagnoses.
|
|
199
|
+
sourceId?: string;
|
|
200
|
+
// ICD code(s) (e.g. ["I25.10"]) from a report's coded header Diagnosis list.
|
|
201
|
+
// A coarse coded comorbidity that a more-detailed finding already covers is
|
|
202
|
+
// collapsed into that finding, so a row can carry more than one code. Absent
|
|
203
|
+
// for plain study findings and hand-entered diagnoses.
|
|
204
|
+
icdCodes?: string[];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// One ingested source file — a lab xlsx, a DEXA/scale export, or a narrative
|
|
208
|
+
// medical report PDF. The bytes live as a committed file at `file`; this record
|
|
209
|
+
// links the readings/diseases it produced (by `id` === their `sourceId`) back to
|
|
210
|
+
// the document, and is the content-hash dedup key for re-ingest.
|
|
211
|
+
export interface SourceRecord {
|
|
212
|
+
id: string; // sha256(bytes).slice(0,12) — also the entries' sourceId
|
|
213
|
+
sha256: string; // full content hash (dedup key)
|
|
214
|
+
kind: "lab" | "dexa" | "scale" | "imaging";
|
|
215
|
+
file: string; // repo-relative path to the stored source file
|
|
216
|
+
originalName: string;
|
|
217
|
+
importedAt: string;
|
|
218
|
+
model?: string; // imaging only (the extraction model)
|
|
219
|
+
mode?: string; // imaging only
|
|
220
|
+
studyType?: string; // imaging only
|
|
221
|
+
studyDate?: string; // imaging only
|
|
222
|
+
dateStart?: string; // lab/dexa/scale — earliest reading date in the file
|
|
223
|
+
dateEnd?: string; // lab/dexa/scale — latest reading date
|
|
224
|
+
extraction?: ImagingExtraction; // imaging only — cached LLM output for deterministic re-import
|
|
225
|
+
diseaseCount?: number;
|
|
226
|
+
markerCount?: number;
|
|
227
|
+
readingCount?: number; // lab/dexa/scale
|
|
228
|
+
// A report is the one AI-adjacent section that ALREADY had a real source record, so its pin
|
|
229
|
+
// lives on the record itself rather than in the itemRegistry below. Pinning never edits the
|
|
230
|
+
// report's content, only marks it an area of query for the next finding.
|
|
231
|
+
pinned?: boolean;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// The cached output of a report extraction (mirrors the proposal shape a caller assembles;
|
|
235
|
+
// declared here so it can live in the vault).
|
|
236
|
+
export interface ImagingExtraction {
|
|
237
|
+
studyType: string;
|
|
238
|
+
diseases: { date: string; diagnostic: string; summary?: string; confidence: number }[];
|
|
239
|
+
comorbidities?: { code: string; label: string; description?: string; confidence: number }[];
|
|
240
|
+
priorComparisons?: { marker: string; priorValue: number; priorDate: string; currentValue: number; unit: string; confidence: number }[];
|
|
241
|
+
markers: { marker: string; value: number; unit: string; date: string; group: string; confidence: number }[];
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export interface DecisionEntry {
|
|
245
|
+
id: string;
|
|
246
|
+
pinned?: boolean;
|
|
247
|
+
intervention: string;
|
|
248
|
+
purpose: string;
|
|
249
|
+
attachments?: Attachment[];
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export interface AllergyEntry {
|
|
253
|
+
id: string;
|
|
254
|
+
pinned?: boolean;
|
|
255
|
+
allergen: string;
|
|
256
|
+
reaction: string;
|
|
257
|
+
severity?: "mild" | "moderate" | "severe";
|
|
258
|
+
dateNoted?: string;
|
|
259
|
+
attachments?: Attachment[];
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export interface FamilyHistoryEntry {
|
|
263
|
+
id: string;
|
|
264
|
+
pinned?: boolean;
|
|
265
|
+
relation: string;
|
|
266
|
+
condition: string;
|
|
267
|
+
attachments?: Attachment[];
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// M-annotate — a row from Chat/Markers/Reports/Study/Hypothesis attached to a to-be-created note,
|
|
271
|
+
// mirroring chat-threads.ts's ReferenceTurnData shape (permalink + preview) but with its own kind
|
|
272
|
+
// union (one member per source view) instead of ReferenceKind, since it seeds a note rather than a
|
|
273
|
+
// chat reference card and reference-resolver.ts already imports from this file (importing
|
|
274
|
+
// ReferenceKind back here would cycle).
|
|
275
|
+
// "treatment"/"note"/"allergy"/"family" added as Annotate went from covering a handful of leaf
|
|
276
|
+
// types to universal (every leaf action menu).
|
|
277
|
+
/**
|
|
278
|
+
* A pointer to somewhere in the record, structurally.
|
|
279
|
+
*
|
|
280
|
+
* types.ts used to import a `Permalink` type from a host-side module for this one field, which
|
|
281
|
+
* dragged the host app's UI ROUTING (its tab union, its section tables) into a type module meant to
|
|
282
|
+
* be host-agnostic.
|
|
283
|
+
*
|
|
284
|
+
* `tab` is `string` here rather than a host's own tab union, and that widening is the whole mechanism:
|
|
285
|
+
* a `Permalink` remains assignable to a `LeafRef`, so every writer is unchanged, while a reader that
|
|
286
|
+
* genuinely needs the union narrows at its own boundary. The brain does not get an opinion about how
|
|
287
|
+
* many tabs the app has.
|
|
288
|
+
*/
|
|
289
|
+
export interface LeafRef {
|
|
290
|
+
client?: string;
|
|
291
|
+
tab: string;
|
|
292
|
+
section?: string;
|
|
293
|
+
anchor?: string;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export interface NoteAttachment {
|
|
297
|
+
kind: "chatTurn" | "marker" | "report" | "study" | "idea" | "group" | "treatment" | "note" | "allergy" | "family";
|
|
298
|
+
permalink: LeafRef;
|
|
299
|
+
preview: { title: string; subtitle?: string; tag: string };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export interface NoteEntry {
|
|
303
|
+
id: string;
|
|
304
|
+
pinned?: boolean;
|
|
305
|
+
text: string;
|
|
306
|
+
// A reference card pointing BACK at another leaf this note was created from (Chat/Markers/
|
|
307
|
+
// Reports/etc — see NoteAttachment below), singular by construction (Annotate seeds exactly one).
|
|
308
|
+
attachment?: NoteAttachment;
|
|
309
|
+
// Photos/documents uploaded ONTO this note (unrelated to `attachment` above).
|
|
310
|
+
attachments?: Attachment[];
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export interface FindingDecisionEntry {
|
|
314
|
+
intervention: string;
|
|
315
|
+
purpose: string;
|
|
316
|
+
pros: string[];
|
|
317
|
+
cons: string[];
|
|
318
|
+
alternatives: string[];
|
|
319
|
+
recommendation: string;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export interface ClientFactors {
|
|
323
|
+
diseases?: DiseaseEntry[];
|
|
324
|
+
// The unified heterogeneous treatment list (drugs, supplements, behaviors),
|
|
325
|
+
// each temporally bucketed by start/end. Supersedes the retired medications /
|
|
326
|
+
// supplements / plan arrays; un-migrated vaults are folded via treatmentsOf().
|
|
327
|
+
treatments?: TreatmentItem[];
|
|
328
|
+
allergies?: AllergyEntry[];
|
|
329
|
+
familyHistory?: FamilyHistoryEntry[];
|
|
330
|
+
decisions?: DecisionEntry[];
|
|
331
|
+
noteEntries?: NoteEntry[];
|
|
332
|
+
pregnancy?: "none" | "pregnant" | "postpartum" | "menopause";
|
|
333
|
+
athletic?: "sedentary" | "moderate" | "endurance";
|
|
334
|
+
bmi?: number;
|
|
335
|
+
height?: string;
|
|
336
|
+
smoking?: "never" | "former" | "current";
|
|
337
|
+
ethnicity?: string;
|
|
338
|
+
goal?: string;
|
|
339
|
+
focus?: string;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export interface StudyEntry {
|
|
343
|
+
id: string;
|
|
344
|
+
pinned?: boolean;
|
|
345
|
+
focus: string;
|
|
346
|
+
detail: string;
|
|
347
|
+
attachments?: Attachment[];
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export interface ClientStudy {
|
|
351
|
+
// Named study tuples (e.g. focus "Selection", "Suspicion") rendered as rows in Pursued Study.
|
|
352
|
+
entries?: StudyEntry[];
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export interface FindingBasis {
|
|
356
|
+
patientAssessment: string;
|
|
357
|
+
patientProfile: string;
|
|
358
|
+
statedObjective: string;
|
|
359
|
+
pursuedStudy: string;
|
|
360
|
+
pursuedNotes: string;
|
|
361
|
+
diagnosedDisease: string;
|
|
362
|
+
treatmentHistory: string;
|
|
363
|
+
correlationHistory: string;
|
|
364
|
+
patientHypothesis: string;
|
|
365
|
+
markerLevels: string;
|
|
366
|
+
aiFindings: string;
|
|
367
|
+
healthProgression: string;
|
|
368
|
+
studyResults: string;
|
|
369
|
+
noteResults: string;
|
|
370
|
+
possibleFindings: string;
|
|
371
|
+
treatmentAssessment: string;
|
|
372
|
+
dataRequisition: string;
|
|
373
|
+
aiHypothesis: string;
|
|
374
|
+
hypothesisEvaluation: string;
|
|
375
|
+
doctorConversation: string;
|
|
376
|
+
healthMarkers: string;
|
|
377
|
+
patientPlan: string;
|
|
378
|
+
patternAntipattern: string;
|
|
379
|
+
clinicalSynthesis: string;
|
|
380
|
+
criticalRatios: string;
|
|
381
|
+
aiOnPlan: string;
|
|
382
|
+
finalThoughts: string;
|
|
383
|
+
abbreviations: string;
|
|
384
|
+
treatmentGroups: string;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// The AI's partition of all proposed treatments into priority-ordered groups, each rendered as
|
|
388
|
+
// one shared bubble in Future Treatment. Refs are verbatim: `patient` entries match a
|
|
389
|
+
// factors.decisions[].intervention OR a PLANNED treatment's label (its name + dose); `ai`
|
|
390
|
+
// entries match a finding.decisions.ai[].intervention. Many-to-many (a group may have several of each) and
|
|
391
|
+
// either side may be empty (AI-only or patient-only group).
|
|
392
|
+
export interface TreatmentGroup {
|
|
393
|
+
system: string; // the body system this cluster sits under; matches a disease[].group verbatim
|
|
394
|
+
topic: string; // the drug class of the cluster (not a speculative benefit)
|
|
395
|
+
patient: string[];
|
|
396
|
+
ai: string[];
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// A clinically meaningful ratio of two markers (e.g. Triglycerides : HDL), chosen
|
|
400
|
+
// by the Finding from the patient's challenges. Carries BOTH a general (population /
|
|
401
|
+
// guideline) range and a personalized range with explanations — so the dashboard can
|
|
402
|
+
// render it "just like any other marker" — plus the plain-language meaning. Value and
|
|
403
|
+
// trend are computed by the UI from the two component series, paired by quarter
|
|
404
|
+
// (components are not always drawn on the same day). See `marker-ratios.ts`.
|
|
405
|
+
export interface CriticalRatio {
|
|
406
|
+
name: string; // display name, e.g. "Triglycerides : HDL"
|
|
407
|
+
numerator: string; // component marker name — must match a marker in client.results
|
|
408
|
+
denominator: string; // component marker name — must match a marker in client.results
|
|
409
|
+
unit: string; // ratio unit label; "" for a dimensionless ratio
|
|
410
|
+
meaning: string; // what the ratio signifies for this patient
|
|
411
|
+
generalLow?: number;
|
|
412
|
+
generalHigh?: number;
|
|
413
|
+
generalExplanation: string;
|
|
414
|
+
personalizedLow?: number;
|
|
415
|
+
personalizedHigh?: number;
|
|
416
|
+
explanation: string; // rationale for the personalized target
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
export interface ClientFinding {
|
|
420
|
+
progression: {
|
|
421
|
+
latest: string;
|
|
422
|
+
recent: string;
|
|
423
|
+
overall: string;
|
|
424
|
+
};
|
|
425
|
+
// One inference per populated Pursued Study row (each named study tuple);
|
|
426
|
+
// `study` is the row label, `result` the prose.
|
|
427
|
+
// `group` tags the body system (a disease[].group verbatim) so the UI
|
|
428
|
+
// can render study lines under System Analysis headings; optional so older
|
|
429
|
+
// findings without it still render (StudyResults falls back to a flat list).
|
|
430
|
+
studyResults?: { study: string; result: string; group?: string }[];
|
|
431
|
+
// One inference per populated Note row (factors.noteEntries); keyed by `noteId` rather than a
|
|
432
|
+
// label (M92 — unlike Study's short hand-picked `focus` labels, a note's `text` is unbounded
|
|
433
|
+
// free prose, unreliable for the LLM to echo back verbatim for pairing).
|
|
434
|
+
noteResults?: { noteId: string; result: string; group?: string }[];
|
|
435
|
+
// M94/M97 §C — one inference per known allergy (factors.allergies) / family history entry
|
|
436
|
+
// (factors.familyHistory), keyed by id like noteResults, paired by array position (no natural
|
|
437
|
+
// unique label to echo back).
|
|
438
|
+
allergyResults?: { allergyId: string; result: string; group?: string }[];
|
|
439
|
+
familyResults?: { familyId: string; result: string; group?: string }[];
|
|
440
|
+
// M102 — mirrors allergyResults/familyResults exactly, for Reports diagnoses (factors.diseases),
|
|
441
|
+
// which previously had no AI-paired leaf at all.
|
|
442
|
+
diseaseResults?: { diseaseId: string; result: string; group?: string }[];
|
|
443
|
+
disease: { group: string; finding: string }[];
|
|
444
|
+
// `group` tags the body system this treatment targets (a disease[].group
|
|
445
|
+
// verbatim) so Current Treatment groups by System Analysis; optional for older findings without it.
|
|
446
|
+
// `phase` (per-bucket assessments) says WHICH slice of a drug's history this entry is about, so one
|
|
447
|
+
// drug can carry up to three: its past arc, its current dose, its planned escalation. Optional
|
|
448
|
+
// because every entry written before this existed is phase-less; such an entry still renders under
|
|
449
|
+
// every bucket, exactly as it did, until that drug is next translated. See assessmentFor().
|
|
450
|
+
/**
|
|
451
|
+
* `treatmentId` echoes the TreatmentItem.id this assessment is ABOUT.
|
|
452
|
+
*
|
|
453
|
+
* noteResults/allergyResults/familyResults/diseaseResults were converted to id-echo pairing
|
|
454
|
+
* earlier, leaving this section keyed on `item`, a name string the model writes with the dose appended
|
|
455
|
+
* ("Rosuvastatin 20 mg"). Everything downstream then had to guess: matchByTreatmentName tries three
|
|
456
|
+
* substring rules in order, a `used` set stops one row being claimed twice, and a rename has to
|
|
457
|
+
* splice the new name into the stored string. treatment-bucket.ts took 25 changes in 403 lines —
|
|
458
|
+
* the highest churn density in the codebase — and its history is a run of fixes to that guessing.
|
|
459
|
+
*
|
|
460
|
+
* OPTIONAL, because every Finding already in a vault was written without it. `assessmentFor`
|
|
461
|
+
* prefers the id and falls back to the name rules, so stored answers keep resolving and the next
|
|
462
|
+
* regeneration of a row upgrades it — the same legacy-read shim pattern as TreatmentItem.images.
|
|
463
|
+
*/
|
|
464
|
+
treatment: { item: string; treatmentId?: string; assessment: string; group?: string; phase?: "past" | "ongoing" | "planned" }[];
|
|
465
|
+
// Patient-specific clinical patterns / anti-patterns surfaced in the AI
|
|
466
|
+
// Conclusion (distinct from the static methodology Introduction on page 1).
|
|
467
|
+
patternAntipattern?: { pattern: string; antipattern: string };
|
|
468
|
+
// Two-track trajectory synthesis: forces working against the patient
|
|
469
|
+
// (structural / heritable / age-clock) vs gains working in their favor
|
|
470
|
+
// (lifestyle- and treatment-driven), plus an optional qualitative
|
|
471
|
+
// biological-vs-chronological read where the data supports one. Synthesizes
|
|
472
|
+
// findings already established elsewhere in the report — it does not diagnose.
|
|
473
|
+
// Optional so findings generated before this field existed still render.
|
|
474
|
+
clinicalSynthesis?: { adverse: string; favorable: string; conditioning?: string };
|
|
475
|
+
// Clinically meaningful marker ratios chosen from the patient's challenges,
|
|
476
|
+
// rendered both as the "Critical Ratios" report section and as the dashboard
|
|
477
|
+
// "Marker Ratios" cards. Optional so findings generated before this feature render.
|
|
478
|
+
criticalRatios?: CriticalRatio[];
|
|
479
|
+
decisions?: {
|
|
480
|
+
patient: FindingDecisionEntry[];
|
|
481
|
+
ai: FindingDecisionEntry[];
|
|
482
|
+
};
|
|
483
|
+
doctorConversation: { group: string; questions: string[] }[];
|
|
484
|
+
definitions: { term: string; definition: string; group: string }[];
|
|
485
|
+
healthMarkers: {
|
|
486
|
+
recommended: { group: string; markers: { name: string; rationale: string }[] }[];
|
|
487
|
+
};
|
|
488
|
+
// `group` tags the body system this requisition cell informs (a disease[].group verbatim) so
|
|
489
|
+
// Tests to Consider groups by System Analysis; optional for older findings without it (flat modality fallback).
|
|
490
|
+
dataRequisition?: { type: string; group?: string; items: string[] }[];
|
|
491
|
+
// AI-generated grouping of patient + AI proposed treatments (Future Treatment). Optional so
|
|
492
|
+
// older findings without it still render (the UI falls back to a client-side heuristic).
|
|
493
|
+
treatmentGroups?: TreatmentGroup[];
|
|
494
|
+
planAssessment?: string;
|
|
495
|
+
// The AI's assessment of each Patient Plan action individually (holistic summary stays in
|
|
496
|
+
// planAssessment). Optional so older findings without it render (Treatment Plan falls back to holistic).
|
|
497
|
+
planAssessmentRows?: { action: string; assessment: string }[];
|
|
498
|
+
finalThoughts?: string;
|
|
499
|
+
basis?: FindingBasis;
|
|
500
|
+
generatedAt: string;
|
|
501
|
+
inputsHash: string;
|
|
502
|
+
// Per-node input hashes (a Dag node key → 12-hex sha256 of that node's input closure).
|
|
503
|
+
// Drives per-section staleness (staleNodes) and selective regen. Optional so older findings without
|
|
504
|
+
// it still render and fall back to the monolithic inputsHash.
|
|
505
|
+
nodeHashes?: Record<string, string>;
|
|
506
|
+
/**
|
|
507
|
+
* A version-registry key → the version of the reasoning that wrote that section. `core` for
|
|
508
|
+
* the monolithic call, a Dag node key for each leaf. See akesi-pil/ARCHITECTURE.md §8 *The
|
|
509
|
+
* host's job: a brain registry* for the pattern this assumes a host maintains.
|
|
510
|
+
*
|
|
511
|
+
* Distinct from `nodeHashes`, and the pair is the point: nodeHashes answers "were the patient's
|
|
512
|
+
* INPUTS the same", promptVersions answers "was the REASONING the same". A section can be fresh on
|
|
513
|
+
* one and stale on the other, and only together do they say whether a stored answer is reproducible.
|
|
514
|
+
*
|
|
515
|
+
* Optional, and a missing entry means genuinely unknown — every finding written before this field
|
|
516
|
+
* existed has none, and a section carried forward from a failed leaf keeps no version because the
|
|
517
|
+
* reasoning that produced it may since have changed. Never defaulted to the current version: a
|
|
518
|
+
* guess here would silently corrupt exactly the join a regen log exists to make.
|
|
519
|
+
*/
|
|
520
|
+
promptVersions?: Record<string, string>;
|
|
521
|
+
generatedBy?: GeneratedBy;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// The four sections whose items a finding GENERATES: a question, a glossary term, an
|
|
525
|
+
// exploration item, an analysis passage. None of them has a source record to hang a pin on, and
|
|
526
|
+
// none can be given a stable id, because the next finding rewrites the list wholesale. So the
|
|
527
|
+
// record is keyed by the item's own TEXT (normalized) and minted lazily — it exists only once the
|
|
528
|
+
// user has pinned it, and unpinning deletes it again. Nothing here is patient-entered content:
|
|
529
|
+
// losing a row costs a star, never data.
|
|
530
|
+
export type ItemRecordKind = "question" | "glossary" | "exploration" | "analysis" | "recommendedMarkers";
|
|
531
|
+
|
|
532
|
+
export interface ItemRecord {
|
|
533
|
+
kind: ItemRecordKind;
|
|
534
|
+
/** The item's text as it read when pinned — matched case- and whitespace-insensitively, so a
|
|
535
|
+
* regenerated Finding that only reflows the wording keeps the pin. */
|
|
536
|
+
label: string;
|
|
537
|
+
pinned?: boolean;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* One leaf regen, recorded before and after. See akesi-pil/ARCHITECTURE.md §8 *The host's job: a
|
|
542
|
+
* brain registry* for the event-log pattern this assumes a host maintains (the append-only rule
|
|
543
|
+
* and what is deliberately NOT recorded).
|
|
544
|
+
*
|
|
545
|
+
* `before`/`after` hold the Finding sections the merge actually rewrote, so a pair is directly
|
|
546
|
+
* comparable. This is clinical content and therefore PHI: it lives in the vault, encrypted, and must
|
|
547
|
+
* never be written anywhere the Finding itself would not go.
|
|
548
|
+
*/
|
|
549
|
+
export interface RegenEvent {
|
|
550
|
+
at: string;
|
|
551
|
+
/** finding-dag node key. */
|
|
552
|
+
node: string;
|
|
553
|
+
/**
|
|
554
|
+
* The OPERATION that ran, not the reason it was needed. A single-section translate versus a
|
|
555
|
+
* whole-finding refresh is all this system can honestly distinguish.
|
|
556
|
+
*/
|
|
557
|
+
triggeredBy: "translate" | "refresh";
|
|
558
|
+
/** Keys present in `before`/`after`, so a reader need not diff two objects to find the subject. */
|
|
559
|
+
sections: string[];
|
|
560
|
+
before: Record<string, unknown>;
|
|
561
|
+
after: Record<string, unknown>;
|
|
562
|
+
/** The brain registry's entry for this node at the time of the regen; absent when the node has
|
|
563
|
+
* no registered version. */
|
|
564
|
+
brainVersion?: string;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
export interface Client {
|
|
568
|
+
displayName: string;
|
|
569
|
+
dob: string;
|
|
570
|
+
gender: "male" | "female";
|
|
571
|
+
watchlist: string[];
|
|
572
|
+
// Starred Marker Ratios, kept separate from `watchlist` because a ratio is not a tracked
|
|
573
|
+
// raw marker: it never belongs in the marker blocks, and nothing here is patient-entered, and it
|
|
574
|
+
// is never read by the prompt directly. A ratio name is the finding's own
|
|
575
|
+
// output (buildMarkerRatios derives it from finding.criticalRatios), so feeding a starred one
|
|
576
|
+
// back as an AREA OF QUERY tells the model which of its own ratios to look at again. It reaches
|
|
577
|
+
// the prompt through pinned-queries.ts only, never as a reading or a fact.
|
|
578
|
+
// Absent = [] (defaulted at client creation).
|
|
579
|
+
pinnedRatios?: string[];
|
|
580
|
+
// Pins for the items that have no record of their own (see ItemRecord above). Absent = none
|
|
581
|
+
// pinned; a record is appended on the first pin and removed again on unpin.
|
|
582
|
+
itemRegistry?: ItemRecord[];
|
|
583
|
+
recommended?: string[];
|
|
584
|
+
results: MarkerResult[];
|
|
585
|
+
factors?: ClientFactors;
|
|
586
|
+
study?: ClientStudy;
|
|
587
|
+
personalizedRanges?: Record<string, PersonalizedRange>;
|
|
588
|
+
finding?: ClientFinding;
|
|
589
|
+
// AI grouping of EVERY distinct marker (all sources: blood, scan, scale)
|
|
590
|
+
// into the patient's body systems (finding.disease[].group verbatim), so the
|
|
591
|
+
// Markers UI groups by System Analysis rather than by the lab panel an import
|
|
592
|
+
// happened to name. Cross-source and decoupled from import structure. See
|
|
593
|
+
// system-groups.ts.
|
|
594
|
+
markerGroups?: MarkerGrouping;
|
|
595
|
+
// Append-only history of leaf regens (see akesi-pil/ARCHITECTURE.md §8 *The host's job: a
|
|
596
|
+
// brain registry*). PHI: it holds finding sections verbatim. Absent on every client that
|
|
597
|
+
// predates the feature, and on one that has never regened.
|
|
598
|
+
regenLog?: RegenEvent[];
|
|
599
|
+
factorsHash?: string;
|
|
600
|
+
sources?: SourceRecord[];
|
|
601
|
+
// PHI-free tombstones for removed sources. A removal hard-deletes the
|
|
602
|
+
// SourceRecord, its raw/processed files, and every derived datum it produced;
|
|
603
|
+
// this leaves only a non-identifying "something was removed, when" record
|
|
604
|
+
// (motivating case: a wrong-patient file ingested by mistake — expunge it and
|
|
605
|
+
// prove it's gone). Re-ingesting the same sha clears its tombstone.
|
|
606
|
+
removedSources?: RemovedSource[];
|
|
607
|
+
// A host's per-patient visibility policy: feature key → shown to the
|
|
608
|
+
// patient's own session. Absent key = the host's catalog default. The
|
|
609
|
+
// provider sees everything regardless; this only narrows a patient's own view. Stored in
|
|
610
|
+
// the patient vault (the patient decrypts it), so it's a product-surface control, not a
|
|
611
|
+
// secret from a technical patient.
|
|
612
|
+
patientVisibility?: Record<string, boolean>;
|
|
613
|
+
// Files uploaded via the browser that the inline path can't process yet
|
|
614
|
+
// (non-PDF: lab xlsx, DEXA, scale, unknown). The raw bytes are already in a host's object store; the
|
|
615
|
+
// CLI `ingest --process-pending` parses + folds them, then removes the entry. Held
|
|
616
|
+
// separate from sources[] because the kind/readings are unknown until processed.
|
|
617
|
+
pendingUploads?: PendingUpload[];
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// A browser upload awaiting out-of-band (CLI) processing. Content-addressed like a
|
|
621
|
+
// source, but with no parsed data yet — the "~24h" queue.
|
|
622
|
+
export interface PendingUpload {
|
|
623
|
+
id: string; // sha256.slice(0,12), same key space as SourceRecord.id
|
|
624
|
+
sha256: string;
|
|
625
|
+
file: string; // the raw's provisional name under raw/{id}/ (<sha8>-<safeName>)
|
|
626
|
+
originalName: string;
|
|
627
|
+
uploadedAt: string;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
export interface RemovedSource {
|
|
631
|
+
sourceId: string; // the removed SourceRecord.id
|
|
632
|
+
sha8: string; // first 8 of the content hash (dedup key, non-identifying)
|
|
633
|
+
kind: string; // lab | dexa | scale | imaging
|
|
634
|
+
removedAt: string;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
export interface MarkerGrouping {
|
|
638
|
+
// Each group's `group` is a finding.disease[].group name VERBATIM, or the
|
|
639
|
+
// trailing "Not yet categorized" bucket (UNCATEGORIZED in system-groups.ts).
|
|
640
|
+
groups: { group: string; markers: string[] }[];
|
|
641
|
+
// sha256 (12-char) over the sorted distinct-marker set AND the sorted disease
|
|
642
|
+
// group set at generation time, so a new marker OR a changed System Analysis is
|
|
643
|
+
// detected and regrouped.
|
|
644
|
+
markerGroupsHash: string;
|
|
645
|
+
generatedAt: string;
|
|
646
|
+
generatedBy: GeneratedBy;
|
|
647
|
+
}
|