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