Описывает что происходит с системой и UI когда пациент загружает несколько тестов в разные моменты времени и взаимодействует с HealthReport. Покрывает actuality classifier поведение, snapshot navigation, разницу между страницами (test detail vs HealthReport vs Analyses), и retrospective time-travel.
Сценарий fictional — биомаркер-значения и даты придуманы для иллюстрации. Конкретный пациент в продакшне может выглядеть иначе.
Ключевая дизайн-позиция (Q7, 2026-05-19)
Интерпретация всегда patient-wide. Нет отдельного test-scoped режима. Pipeline одинаковый для:
- Запуска с HealthReport (
/health) — asOfDate = max(obs.date) по умолчанию (свежий снимок) - Запуска с test detail page (
/desk/<id>/test/<testId>/results) — asOfDate = test.testDate (retrospect на дату теста)
Отличается только asOfDate (cutoff по времени). Все биомаркеры с obs.date ≤ asOfDate идут в latest-per-LOINC; все активные на asOfDate Conditions/Medications учитываются как контекст; actuality classifier судит относительно asOfDate.
Test detail page — это filtered VIEW на patient-wide данные: показывает только биомаркеры из текущего теста, prose тянется из patient-level CI sub-extensions.
Почему так — простой путь, один pipeline, один storage, никаких отдельных test-scoped Composition/CarePlan/CI. Если test-scoped рамка понадобится (например, для legacy B2B-клиента с узким контрактом) — это будет отдельное решение, не дефолт.
Пациент
Анна, 52 года, женщина.
Активные диагнозы (FHIR Condition со clinicalStatus = active):
Condition/anna-t2dm— Type 2 Diabetes Mellitus (onsetDateTime 2024-03)Condition/anna-htn— Hypertension (onsetDateTime 2023-11)
Активные лекарства (FHIR MedicationStatement):
MedicationStatement/anna-metformin— Metformin 1000mg BID (effectivePeriod start 2024-03)MedicationStatement/anna-lisinopril— Lisinopril 10mg (effectivePeriod start 2023-11)
Тесты которые она загружает
Три PDF-документа за разные годы. Биомаркер-наборы НЕ совпадают между тестами — реальная ситуация когда лаборатория сделала разные панели в разные годы.
Test 1 — 2022-06-15 (за 2 года до T2DM)
Базовая профилактика. Биомаркеры:
| Биомаркер | Value | Unit | Reference | Status |
|---|---|---|---|---|
| Hemoglobin | 13.8 | g/dL | 12-16 | Normal |
| Erythrocytes | 4.6 | млн/мкл | 3.8-5.2 | Normal |
| Glucose | 5.2 | mmol/L | 3.9-6.1 | Normal |
| LDL Cholesterol | 130 | mg/dL | 0-130 | Borderline |
| HDL Cholesterol | 55 | mg/dL | 40-100 | Normal |
| Total Cholesterol | 210 | mg/dL | 0-200 | Slightly High |
| ALT | 24 | U/L | 0-40 | Normal |
Test 2 — 2024-09-10 (после диагноза T2DM, на терапии 6 мес)
Контрольный, focused на metabolic markers. Не все из предыдущего теста повторены.
| Биомаркер | Value | Unit | Reference | Status |
|---|---|---|---|---|
| Glucose | 7.8 | mmol/L | 3.9-6.1 | High |
| HbA1c | 7.5 | % | 4.0-5.6 | High |
| LDL Cholesterol | 145 | mg/dL | 0-130 | High |
| Triglycerides | 220 | mg/dL | 0-150 | High |
(Hgb, RBC, HDL, Total Cholesterol, ALT — НЕ пересдавались.)
Test 3 — 2026-04-26 (свежий, годовой контроль)
| Биомаркер | Value | Unit | Reference | Status |
|---|---|---|---|---|
| Glucose | 8.2 | mmol/L | 3.9-6.1 | High |
| HbA1c | 8.1 | % | 4.0-5.6 | High |
| LDL Cholesterol | 168 | mg/dL | 0-130 | High |
| HDL Cholesterol | 32 | mg/dL | 40-100 | Low |
| Triglycerides | 250 | mg/dL | 0-150 | High |
| Total Cholesterol | 235 | mg/dL | 0-200 | High |
| ALT | 38 | U/L | 0-40 | Normal |
(Hgb, RBC — снова не пересдавались.)
Шаг 1 — Загрузка и распознавание
Анна загружает три PDF через /desk/<patientId>/upload. Каждый файл проходит recognition pipeline:
- Upload →
BloodTestFileв Postgres + объект в GCS - Recognition: VLM extracts biomarker values → нормализация LOINC
- FHIR write: для каждого теста —
DiagnosticReport+ NObservationresources - Postgres tracking:
BloodTestrow со ссылкой наfhirDiagnosticReportId,status = 'completed'
FHIR state после загрузки (приблизительно):
- 3×
DiagnosticReport(по одному на тест) —DiagnosticReport/anna-test-2022,.../anna-test-2024,.../anna-test-2026 - 18×
Observationresources — каждый upload добавляет свои Observations отдельно, никакой дедупликации на этой стадии. У Анны 7+4+7 = 18 биомаркер-измерений по разным дням, все живут в FHIR как отдельные ресурсы. (Дедупликация «один latest на LOINC» — runtime операция pipeline’а, не persistence; см. шаг 3.) Patient/anna-001(с demographics)- 2×
Condition(anna-t2dm,anna-htn) - 2×
MedicationStatement(anna-metformin,anna-lisinopril)
Что Анна видит сразу: на /desk/<patientId> — список из 3 тестов с датами и статусом «completed». Никакой интерпретации ещё нет (HealthReport пуст).
Шаг 2 — Что видно на каждой странице (без интерпретации)
Test detail page — /desk/<id>/test/<testId>/results
Три отдельных страницы (по одной на тест). Каждая показывает ТОЛЬКО биомаркеры этого теста, как лаба напечатала. Все биомаркеры теста актуальны на дату теста — никакой actuality-фильтрации не происходит, никакой амбер-секции нет.
Например на странице Test 2 (2024-09-10):
- Glucose 7.8 mmol/L · High
- HbA1c 7.5% · High
- LDL 145 mg/dL · High
- Triglycerides 220 mg/dL · High
Каждый со sparkline истории (если есть исторические значения того же LOINC у пациента) и описанием.
Test detail page не показывает AI-prose до тех пор пока не запустится интерпретация. Сразу после upload prose пуста.
Analyses aggregate — /desk/<id>/analyses
ОДИН экран, все биомаркер-наблюдения по всем тестам сразу. Группировка по панелям (Hematology, Biochemistry, Lipid). Внутри каждой панели — все значения с датами.
Полный список для Анны:
Hematology:
- Hemoglobin: 13.8 (2022-06-15) — sparkline из одной точки
- Erythrocytes: 4.6 (2022-06-15) — sparkline из одной точки
Biochemistry:
- Glucose: 5.2 (2022-06-15) → 7.8 (2024-09-10) → 8.2 (2026-04-26) — sparkline видно тренд
- HbA1c: 7.5 (2024-09-10) → 8.1 (2026-04-26)
- ALT: 24 (2022-06-15) → 38 (2026-04-26)
Lipid Profile:
- LDL Cholesterol: 130 (2022) → 145 (2024) → 168 (2026)
- HDL Cholesterol: 55 (2022) → 32 (2026)
- Triglycerides: 220 (2024) → 250 (2026)
- Total Cholesterol: 210 (2022) → 235 (2026)
Шапка показывает aggregate-счётчики: «N parameters / X normal / Y borderline / Z abnormal». Счёт group-by биомаркер (один row на LOINC, не на каждое наблюдение) — но без учёта актуальности. То есть в шапке могут быть нормальные значения от 2022 года, которые сегодня — likely_outdated по мнению classifier’а; на этой странице classifier не задействован, шапка считает по reference-range от лабы. См. health-report-pipeline для framing’а scope axis.
HealthReport — /desk/<id>/health
Пусто до первого запуска интерпретации. Только кнопка «Run analysis».
Шаг 3 — Анна нажимает «Run analysis» на HealthReport
Кнопка триггерит health-report.function.ts pipeline. Параметр asOfDate по умолчанию = max(obs.date среди raw observations) = 2026-04-26 (дата Test 3). Это специально — если по умолчанию использовать today, и тесты старые — actuality classifier пометит всё как outdated.
Pipeline ход
-
buildPatientContextFromFhir — собрать Patient + Conditions + Medications + все Observations (18 штук в FHIR у Анны).
-
dedupLatestPerBiomarker(in-code name; кандидат на renamelatestPerBiomarkerAsOf— обсуждается, не сделано) — фильтрobs.date <= asOfDate+ dedup на «один latest на LOINC». Результат для Анны при asOfDate=2026-04-26:Биомаркер Source test Value Hemoglobin Test 1 (2022-06-15) 13.8 Erythrocytes Test 1 (2022-06-15) 4.6 Glucose Test 3 (2026-04-26) 8.2 HbA1c Test 3 (2026-04-26) 8.1 LDL Cholesterol Test 3 (2026-04-26) 168 HDL Cholesterol Test 3 (2026-04-26) 32 Triglycerides Test 3 (2026-04-26) 250 Total Cholesterol Test 3 (2026-04-26) 235 ALT Test 3 (2026-04-26) 38 9 биомаркеров, latest-per-LOINC. Старые значения (Glucose 5.2 из 2022, LDL 130 из 2022, etc.) НЕ участвуют в актуальном анализе. Они живут в FHIR как Observation history и видимы на Analyses page как точки sparkline, но pipeline их в работе не использует.
Carry-over (Артур, понедельник 2026-05-12): в текущем pipeline ранее-измеренные значения того же биомаркера не передаются в Reasoner как trend-контекст. Это известный gap — Reasoner получает только latest value без истории. Тренды генерируются отдельно и не feed в основную интерпретацию.
-
Diagnostician + Retriever — построить план + достать graph-связи. Retriever ищет какие из активных Conditions/Medications пациента влияют на каждый биомаркер. Для Glucose / HbA1c → нашёл T2DM + Metformin как drivers; для HDL → нашёл T2DM как driver; для Hemoglobin → ничего активного по эту дату не нашёл.
-
classifyPatient — actuality verdict per biomarker. Имя функции неудачное (это не «классификация пациента», а классификация actuality per биомаркер) — кандидат на rename
classifyActualityPerBiomarker. Verdicts для Анны:Биомаркер testDate Возраст к asOfDate Verdict Rationale Hemoglobin 2022-06-15 3.9 года likely_outdatedHematology shelf ~30-90d; 3.9 года >> shelf Erythrocytes 2022-06-15 3.9 года likely_outdatedто же Glucose 2026-04-26 0d currently_representativeсвежее значение HbA1c 2026-04-26 0d currently_representativeсвежее LDL 2026-04-26 0d currently_representative_with_caveatсвежее, но Metformin может влиять косвенно (caveat) HDL 2026-04-26 0d currently_representative_with_caveatто же Triglycerides 2026-04-26 0d currently_representativeсвежее Total Cholesterol 2026-04-26 0d currently_representativeсвежее ALT 2026-04-26 0d currently_representativeсвежее -
filterByActuality — drop
likely_outdated. Keeprepresentative,_with_caveat,lifelong_no_retest(passes through как representative — per 2026-05-19 decision),unknown(fail-open). Hemoglobin и Erythrocytes уходят вoutdatedObservations[]; остальные 7 идут вkeptAbnormalилиkeptNormal. -
Stage 3 — Reasoner + DoctorWriter + Personalizer (per Variant B —
[[../technical/normal-biomarker-pipeline-coverage]]decision, 2026-05-18): все 7 survivors через Reasoner+Writer (нормалы тоже, не только аномалы — это и есть Variant B); Personalizer работает только на abnormal’ах. -
Stage 2 — sub-extension записи без prose для outdated. Hemoglobin и Erythrocytes получают CI sub-extension с metadata (value/unit/range/testDate/actualityStatus =
likely_outdated), но БЕЗclinicalInterpretation(поле пустое — потому что Reasoner+Writer на них не запускались). Это просто «sub-extension с пустым prose», не отдельный mechanism. Сделано чтобы UI знал что биомаркер существует и с каким verdict’ом — даже без AI-описания. -
FHIR write — один patient-level
ClinicalImpressionс 9 sub-extensions (7 с prose от Reasoner+Writer + 2 без prose для outdated). ОдинComposition(overview prose). ОдинCarePlan(priority tests — генерится отдельным enrichment, не обязательно в том же step.run; см.enrichment/follow-up.requested). Conditional PUT по identifier — для Compositionhttp://bloodgpt.com/fhir/identifier/patient-summary|Patient-anna-001. Если впервые — POST, новая versionId; если повтор — увеличивает versionId.
Что Анна видит на HealthReport после запуска
URL: /desk/<patientId>/health
Содержимое сверху вниз:
-
Header: «My Health» · v1 (1/1) · Data as of 2026-04-26 · Last updated: 19/05/2026 17:43
-
Interpretation prose — синтез по 7 биомаркерам в категориях:
- Pattern: Hyperglycemia + atherogenic dyslipidemia (LDL ↑, HDL ↓, TG ↑)
- Health considerations: gradual progression of T2DM despite Metformin, cardiovascular risk
- Recommended steps: HbA1c trend monitoring, cardio risk evaluation
-
Pay Attention badges — список abnormal-биомаркеров с severity
-
Main biomarker panels (группировка по семейству):
- Glucose Metabolism — Glucose 8.2 High, HbA1c 8.1 High. Каждая строка: range bar (где значение на шкале нормы), inline AI-prose от DoctorWriter про конкретный биомаркер с учётом T2DM/Metformin контекста, кликабельный Connections блок (граф-связи: «связан с T2DM, влияние Metformin»).
- Lipid Profile — LDL 168 High с caveat-badge, HDL 32 Low с caveat-badge, Triglycerides 250 High, Total Cholesterol 235 High. Caveat-badge на LDL/HDL потому что классификатор пометил их
currently_representative_with_caveat(Metformin может косвенно влиять). - Liver — ALT 38 Normal (просто значение, нет abnormal-prose, но Reasoner+Writer всё равно сгенерили краткую запись «liver function appears preserved despite metabolic load»).
- Hematology — пустая или скрытая (оба её биомаркера ушли в amber-секцию ниже).
Тренды (sparkline предыдущих значений) сейчас не отображаются inline — это известный gap, см.
[[../technical/trend-panel]]и Артур carry-over. Когда тренды будут передаваться в Reasoner — здесь появится мелкая визуализация изменения за период. -
OutdatedParametersSection (amber):
Not reflecting current state · 2 biomarkers — Hemoglobin 13.8 g/dL · 2022-06-15 (3y past, shelf 90d) — Erythrocytes 4.6 млн/мкл · 2022-06-15 (3y past, shelf 90d) Upload a fresh lab to refresh them.
-
Priority Tests (CarePlan / followUp):
- Repeat CBC (Complete Blood Count) — to refresh Hgb/RBC
- Lipid panel monitoring — quarterly
- HbA1c monitoring — every 3 months
asOfDate в header’е = 2026-04-26, не today. Per health-report-versioning-model правило — max(asOfDate) выбирается как default render. Текущая интерпретация привязана к самому свежему тесту, не к моменту нажатия кнопки.
Шаг 4 — Анна открывает Test 2 (2024-09-10) и нажимает «Run analysis» там
Кнопка на test detail page триггерит тот же patient-wide pipeline, только с asOfDate = test.testDate = 2024-09-10. Видно в коде page.tsx:40-44:
«trigger the V2.5 pipeline with
as_of_date = test.testDate(interpret patient state as it would have looked on the date of THIS test).»
Никакой отдельной test-scoped логики нет. Pipeline тот же, scope тот же (patient-wide), отличается только cutoff по дате.
Pipeline с asOfDate=2024-09-10
latestPerBiomarker фильтрует obs.date <= 2024-09-10:
| Биомаркер | Source test | Value |
|---|---|---|
| Hemoglobin | Test 1 (2022-06-15) | 13.8 |
| Erythrocytes | Test 1 (2022-06-15) | 4.6 |
| Glucose | Test 2 (2024-09-10) | 7.8 |
| HbA1c | Test 2 (2024-09-10) | 7.5 |
| LDL | Test 2 (2024-09-10) | 145 |
| Triglycerides | Test 2 (2024-09-10) | 220 |
| HDL | Test 1 (2022-06-15) | 55 |
| Total Cholesterol | Test 1 (2022-06-15) | 210 |
| ALT | Test 1 (2022-06-15) | 24 |
Заметь — HDL, Total Cholesterol, ALT теперь из Test 1 (2022), потому что Test 2 их не измерял, а Test 3 (2026) ещё не наступил относительно asOfDate=2024-09. Это намеренно — pipeline patient-wide, видит ВСЕ доступные на asOfDate биомаркеры пациента, не ограничен биомаркерами Test 2. Альтернатива (узко-test-scoped) была отвергнута в дизайн-обсуждении 2026-05-19 — простой путь = всегда patient-wide.
Actuality verdicts relative to asOfDate=2024-09:
- Hemoglobin 2022 →
likely_outdated(2 года stale) - Erythrocytes 2022 →
likely_outdated - Glucose 2024 →
currently_representative - HbA1c 2024 →
currently_representative - LDL 2024 →
currently_representative_with_caveat(Metformin может влиять) - HDL 2022 →
currently_representative_with_caveat(2 года stale, T2DM emerged in between) - Triglycerides 2024 →
currently_representative - Total Cholesterol 2022 →
likely_outdated(2 года stale, lipids тяжело bind T2DM) - ALT 2022 →
likely_outdated(2 года stale)
Pipeline пишет новую versionId Composition / CarePlan / ClinicalImpression (conditional PUT увеличивает version по identifier). Server-side _history теперь имеет две версии:
- v1: asOfDate=2026-04-26 (от Шага 3)
- v2: asOfDate=2024-09-10 (от Шага 4)
Per health-report-versioning-model: default render — версия с max(asOfDate) = v1 (2026-04-26).
Что Анна видит после второго запуска
Два эффекта одновременно:
1. Test detail page (Test 2) после запуска — теперь показывает per-biomarker AI-prose для биомаркеров теста. Pose читается из patient-level CI v2 sub-extensions, фильтр на UI-уровне («показывай только биомаркеры этого теста»):
- Glucose 7.8 — full Reasoner+Writer prose с учётом T2DM/Metformin contextualized к 2024 дате
- HbA1c 7.5 — full prose
- LDL 145 — full prose с caveat-badge
- Triglycerides 220 — full prose
Биомаркеры из других тестов на этой странице не показываются — это решает UI-фильтр. Хотя patient-wide pipeline их обработал (HDL 2022, ALT 2022 и т.д.), test detail отфильтрует.
2. HealthReport page /health — по умолчанию показывает v1 (2026-04-26), не v2. Per max(asOfDate) rule. Чтобы увидеть retrospective view — нужно использовать Snapshot Navigator (стрелки ‹ › рядом с «v1 (1/2)»). Кликнуть «Older snapshot» → загрузится v2.
На v2 HealthReport показано:
-
Header: v2 (2/2) · Data as of 2024-09-10 · Last updated: 19/05/2026 17:50
-
Interpretation prose — синтезирована для другой картинки: T2DM на полгода Metformin’а, lipids разнятся (некоторые из 2024, некоторые из 2022)
-
Main biomarker panels (без Hgb/RBC, без Total Chol, без ALT — они все в amber)
-
OutdatedParametersSection в этой версии содержит 4 биомаркера:
- Hemoglobin 13.8 (2022) · 2y past
- Erythrocytes 4.6 (2022) · 2y past
- Total Cholesterol 210 (2022) · 2y past
- ALT 24 (2022) · 2y past
В v1 их было только 2 — потому что в 2026 Total Chol и ALT были обновлены (Test 3), они стали current. В retrospect к 2024 — Test 3 ещё не было, оба биомаркера из 2022, оба stale.
-
Priority Tests — могут отличаться: «Repeat Total Cholesterol», «Repeat ALT» (потому что они stale на момент 2024).
Семантика — что только что произошло
Анна видит «как бы система интерпретировала меня в 2024». Не «как я была в 2024 по тогдашним нормам» — система оценивает с current knowledge (актуальные guidelines, current Conditions), но с биомаркерами доступными ≤ 2024.
Полнота picture: pipeline учитывает все биомаркеры пациента на дату теста, не только биомаркеры самой бумажки. То есть retrospect это «patient state на 2024-09-10», не «что показала Test 2». Если кому-то нужна узкая trader-style картинка «только что в этой бумажке» — это test detail page (тот же запуск, но UI-фильтр).
Шаг 5 — Возврат к default render
Анна кликает «Newer snapshot» один раз → возвращается на v1 (2026-04-26). При следующем визите страница откроется именно на v1, потому что max(asOfDate) — правило default render’а.
Если она ещё раз запустит «Run analysis» с HealthReport, asOfDate по умолчанию = max(obs.date) = 2026-04-26 (всё ещё), новая версия v3 будет с тем же asOfDate как v1 — conditional PUT с тем же identifier’ом обновит uppermost версию (новая versionId, тот же resource id). v1 уходит в _history, v3 становится head.
Связано
- health-report-versioning-model — FHIR
_history+ conditional PUT, max(asOfDate) rule — active - biomarker-actuality-service — сервис который выдаёт actuality verdicts — draft
- biomarker-actuality-integration — где actuality встроена в pipeline — draft
- normal-biomarker-pipeline-coverage — Variant B (нормалы через Reasoner+Writer) — active
- actuality-class-ui-rendering — как 5 классов рендерятся на HealthReport — draft
- health-report-pipeline — pipeline shape — active
- trend-panel — как отображались тренды в legacy системе — active
- my-health — продуктовая страница HealthReport — active
- test-analysis-pipeline — legacy test-scoped (для B2B
/api/v1/interpret/:testId, в B2C не используется) — active
Открытые design-вопросы (для отдельных обсуждений)
Тренды per биомаркер в Reasoner-контексте. Сейчас Reasoner получает latest value, без истории предыдущих значений того же биомаркера. Это значит prose не учитывает «улучшение / ухудшение / стабильность за период». Carry-over от созвона с Артуром 2026-05-12. Нужно прописать формат передачи trend-данных в Reasoner.
Caveat surface design. currently_representative_with_caveat сейчас рендерится с амбер «caveat» badge inline в строке. Решение принято имплементацией, не settled. Альтернативы — отдельная мини-секция, иконка с tooltip, цветовая полоска. Подсвечивать ли как-то отдельно — TBD.
Snapshot Navigator на test detail page. Сейчас навигация по версиям доступна только на HealthReport. На test detail page нет аналогичной кнопки. Полезно ли — TBD.
Stub UI rendering. Биомаркеры с пустым clinicalInterpretation (likely_outdated) показываются в OutdatedParametersSection с datebox и без раскрытия. Альтернативы — иконка прямо в строке, otherwise — TBD после Q11 в actuality-class-ui-rendering.
Function rename candidates. В коде живут имена которые не отражают актуальную семантику: dedupLatestPerBiomarker (на самом деле filter+pick latest as-of), classifyPatient (на самом деле per-biomarker actuality). Refactoring candidate, не сделан.
Lifelong inventory. Пока в коде у Анны нет lifelong-биомаркеров. Когда появятся (blood type у реального пациента и т.п.) — нужно подтвердить что они рендерятся inline без специального treatment’а, per 2026-05-19 decision.