Описывает что происходит с системой и 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)

Базовая профилактика. Биомаркеры:

БиомаркерValueUnitReferenceStatus
Hemoglobin13.8g/dL12-16Normal
Erythrocytes4.6млн/мкл3.8-5.2Normal
Glucose5.2mmol/L3.9-6.1Normal
LDL Cholesterol130mg/dL0-130Borderline
HDL Cholesterol55mg/dL40-100Normal
Total Cholesterol210mg/dL0-200Slightly High
ALT24U/L0-40Normal

Test 2 — 2024-09-10 (после диагноза T2DM, на терапии 6 мес)

Контрольный, focused на metabolic markers. Не все из предыдущего теста повторены.

БиомаркерValueUnitReferenceStatus
Glucose7.8mmol/L3.9-6.1High
HbA1c7.5%4.0-5.6High
LDL Cholesterol145mg/dL0-130High
Triglycerides220mg/dL0-150High

(Hgb, RBC, HDL, Total Cholesterol, ALT — НЕ пересдавались.)

Test 3 — 2026-04-26 (свежий, годовой контроль)

БиомаркерValueUnitReferenceStatus
Glucose8.2mmol/L3.9-6.1High
HbA1c8.1%4.0-5.6High
LDL Cholesterol168mg/dL0-130High
HDL Cholesterol32mg/dL40-100Low
Triglycerides250mg/dL0-150High
Total Cholesterol235mg/dL0-200High
ALT38U/L0-40Normal

(Hgb, RBC — снова не пересдавались.)

Шаг 1 — Загрузка и распознавание

Анна загружает три PDF через /desk/<patientId>/upload. Каждый файл проходит recognition pipeline:

  1. Upload → BloodTestFile в Postgres + объект в GCS
  2. Recognition: VLM extracts biomarker values → нормализация LOINC
  3. FHIR write: для каждого теста — DiagnosticReport + N Observation resources
  4. Postgres tracking: BloodTest row со ссылкой на fhirDiagnosticReportId, status = 'completed'

FHIR state после загрузки (приблизительно):

  • DiagnosticReport (по одному на тест) — DiagnosticReport/anna-test-2022, .../anna-test-2024, .../anna-test-2026
  • 18× Observation resources — каждый upload добавляет свои Observations отдельно, никакой дедупликации на этой стадии. У Анны 7+4+7 = 18 биомаркер-измерений по разным дням, все живут в FHIR как отдельные ресурсы. (Дедупликация «один latest на LOINC» — runtime операция pipeline’а, не persistence; см. шаг 3.)
  • Patient/anna-001 (с demographics)
  • Condition (anna-t2dm, anna-htn)
  • 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 ход

  1. buildPatientContextFromFhir — собрать Patient + Conditions + Medications + все Observations (18 штук в FHIR у Анны).

  2. dedupLatestPerBiomarker (in-code name; кандидат на rename latestPerBiomarkerAsOf — обсуждается, не сделано) — фильтр obs.date <= asOfDate + dedup на «один latest на LOINC». Результат для Анны при asOfDate=2026-04-26:

    БиомаркерSource testValue
    HemoglobinTest 1 (2022-06-15)13.8
    ErythrocytesTest 1 (2022-06-15)4.6
    GlucoseTest 3 (2026-04-26)8.2
    HbA1cTest 3 (2026-04-26)8.1
    LDL CholesterolTest 3 (2026-04-26)168
    HDL CholesterolTest 3 (2026-04-26)32
    TriglyceridesTest 3 (2026-04-26)250
    Total CholesterolTest 3 (2026-04-26)235
    ALTTest 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 в основную интерпретацию.

  3. Diagnostician + Retriever — построить план + достать graph-связи. Retriever ищет какие из активных Conditions/Medications пациента влияют на каждый биомаркер. Для Glucose / HbA1c → нашёл T2DM + Metformin как drivers; для HDL → нашёл T2DM как driver; для Hemoglobin → ничего активного по эту дату не нашёл.

  4. classifyPatient — actuality verdict per biomarker. Имя функции неудачное (это не «классификация пациента», а классификация actuality per биомаркер) — кандидат на rename classifyActualityPerBiomarker. Verdicts для Анны:

    БиомаркерtestDateВозраст к asOfDateVerdictRationale
    Hemoglobin2022-06-153.9 годаlikely_outdatedHematology shelf ~30-90d; 3.9 года >> shelf
    Erythrocytes2022-06-153.9 годаlikely_outdatedто же
    Glucose2026-04-260dcurrently_representativeсвежее значение
    HbA1c2026-04-260dcurrently_representativeсвежее
    LDL2026-04-260dcurrently_representative_with_caveatсвежее, но Metformin может влиять косвенно (caveat)
    HDL2026-04-260dcurrently_representative_with_caveatто же
    Triglycerides2026-04-260dcurrently_representativeсвежее
    Total Cholesterol2026-04-260dcurrently_representativeсвежее
    ALT2026-04-260dcurrently_representativeсвежее
  5. filterByActuality — drop likely_outdated. Keep representative, _with_caveat, lifelong_no_retest (passes through как representative — per 2026-05-19 decision), unknown (fail-open). Hemoglobin и Erythrocytes уходят в outdatedObservations[]; остальные 7 идут в keptAbnormal или keptNormal.

  6. 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’ах.

  7. 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-описания.

  8. 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 — для Composition http://bloodgpt.com/fhir/identifier/patient-summary|Patient-anna-001. Если впервые — POST, новая versionId; если повтор — увеличивает versionId.

Что Анна видит на HealthReport после запуска

URL: /desk/<patientId>/health

Содержимое сверху вниз:

  1. Header: «My Health» · v1 (1/1) · Data as of 2026-04-26 · Last updated: 19/05/2026 17:43

  2. 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
  3. Pay Attention badges — список abnormal-биомаркеров с severity

  4. 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 — здесь появится мелкая визуализация изменения за период.

  5. 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.

  6. 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 testValue
HemoglobinTest 1 (2022-06-15)13.8
ErythrocytesTest 1 (2022-06-15)4.6
GlucoseTest 2 (2024-09-10)7.8
HbA1cTest 2 (2024-09-10)7.5
LDLTest 2 (2024-09-10)145
TriglyceridesTest 2 (2024-09-10)220
HDLTest 1 (2022-06-15)55
Total CholesterolTest 1 (2022-06-15)210
ALTTest 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 показано:

  1. Header: v2 (2/2) · Data as of 2024-09-10 · Last updated: 19/05/2026 17:50

  2. Interpretation prose — синтезирована для другой картинки: T2DM на полгода Metformin’а, lipids разнятся (некоторые из 2024, некоторые из 2022)

  3. Main biomarker panels (без Hgb/RBC, без Total Chol, без ALT — они все в amber)

  4. 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.

  5. 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.

Связано

Открытые 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.