Решение 2026-02-14 формулировалось как «ровно один custom extension». По факту к 2026-05 их около десятка (см. «Текущее состояние»). Принцип, который остался в силе: каждое новое поле сначала ищет стандартное место в R4, custom extension — последний резорт; «zero / один» — недостижимая формулировка, цель — минимум и явная оправданность каждого.
Контекст
Пайплайн BloodGPT генерирует структурированный AI-вывод: TestOverview (общий narrative + sections), PanelOverview (на каждую панель), FollowUpRecommendations (когда пересдать), ParameterTrends (тренды по параметрам), isHealthy (флаг отклонения), parameterDetails (текст-комментарий на параметр), requiresDoctorPreparation (нужна ли подготовка перед врачом). Нужно положить всё это в FHIR-модель, при этом FHIR-сервер (Google Healthcare API, см. google-healthcare-api) хочет видеть стандартный R4.
Рассматривали
- Custom resource types — изобретать собственные типа
BloodGPTAnalysis. Не FHIR-совместимо, Google Healthcare API такие ресурсы не примет. - Extensions на DiagnosticReport — складывать всё в
extension[]на стандартном ресурсе. Размывает семантику, мешаетPatient/$everythingагрегировать содержательно. - Composition + CarePlan + Observation с custom extensions для AI-полей — initial Claude-предложение. Каждое наше поле (
isHealthy,parameterDetails,requiresDoctorPreparation, и т.д.) → отдельный custom extension с URLhttp://bloodgpt.com/fhir/StructureDefinition/.... Ильдар подсветил чтоisHealthyуже стандартизирован какObservation.interpretation(H/L/N из v3-ObservationInterpretation ValueSet). - Composition + CarePlan + Observation на чистом R4 — пересмотренный вариант. Все наши AI-поля ложатся на стандартные FHIR-конструкции.
Выбрали: Composition + CarePlan + Observation на чистом R4
Маппинг наших полей на стандартный FHIR:
| Наше поле | FHIR-стандарт | Custom? |
|---|---|---|
TestOverview (narrative + sections) | Composition с nested section[] | нет |
PanelOverview | sub-section в Composition | нет |
FollowUpRecommendations (включая досдачу) | CarePlan.activity[] с kind=ServiceRequest | нет |
ParameterTrends (исторические значения) | Observation с _sort=date (запрос исторических Observations) | нет |
parameterTrendAnalysis (AI-текст про тренд) | Composition Section “Trends” → sub-section per parameter (custom section-type code) | нет (см. ниже) |
isHealthy | Observation.interpretation (H/L/N) | нет |
parameterDetails | Observation.note[].text | нет |
requiresDoctorPreparation | extension на Composition | да, единственный |
На момент решения (2026-02) это давало один custom extension — requiresDoctorPreparation на Composition (стандартного FHIR-аналога для “пациенту нужна особая подготовка перед визитом к врачу” нет). Цель формулировалась как «zero, в крайнем случае один». По факту набор с тех пор вырос — см. «Текущее состояние» ниже. Принцип остался: каждое новое AI-поле сначала ищет стандартное FHIR-место, extension только когда стандартного аналога нет.
Расширение в session 065369a0 (2026-03-14): Composition Section “Trends”
Добавлена пятая секция в Composition, с sub-sections на каждый параметр, в которой хранится trendAnalysis (AI-текст про тренд биомаркера). Используются custom section-type коды (bloodgpt.com/fhir/CodeSystem/section-type → trends, trend-analysis). Это не extension — это standard FHIR pattern (Composition.section с custom code, как и остальные секции). Принцип “минимизация custom extensions” не нарушен; скорее подтверждён выбором “section с custom code” вместо “extension с typed value”.
Почему
- Стандартные поля FHIR работают в любом FHIR-клиенте без нашей schema.
Patient/$everythingкорректно агрегирует. Observation.interpretation— официальный механизм статуса параметра, существует именно под этот use case (см. google-healthcare-api поinterpretationValueSet).Observation.note[]поддерживаетauthorReference— есть кому атрибутировать AI-комментарий (см. authorship-organization-not-device).CarePlan.activityсkind=ServiceRequestнативно описывает “досдай Ferritin через месяц” — ровно наш use case.Composition.section[]поддерживает nested sub-sections, 1:1 ложится на нашу TestOverview→PanelOverview иерархию.
Graceful degradation — это не про «работает / не работает»
Важный нюанс, который часто упускают: extensions сами по себе не ломают interop. Если downstream consumer не знает наш формальный profile (или мы вообще без profile живём, как сейчас), он всё равно прочитает наши ресурсы — потому что они остаются валидным FHIR R4.
Generic FHIR-клиент при чтении Composition с нашим requires-doctor-preparation extension’ом увидит:
- Стандартные поля (status, type, subject, sections) — полная семантика, работает в любом FHIR-парсере.
extension[]— видит как «unknown extension с URL X и valueBoolean true». FHIR-стандарт ОБЯЗЫВАЕТ любой FHIR-клиент уметь парсить extension array, даже если не понимает конкретный URL.
Это forward compatibility built into protocol — аналог «extra fields в JSON response» в обычных API. Можно добавлять extensions, не ломая существующих consumers.
Поэтому решение «минимизировать extensions» — не про «иначе сломается». Это про:
- Понятность. Каждый extension — это URL, по которому consumer должен пойти прочитать что мы там подразумевали. Чем меньше таких URL’ов — тем меньше документации читать.
- Стандартная семантика поверх кастомной. Если можно положить «нездоровый» в
Observation.interpretation(стандартный механизм с known semantics) — это лучше чем класть в свой extension, даже если генерация «бесплатна» в обоих случаях. Consumer без profile knowledge получит больше пользы от стандартного поля. - Discoverability через
Patient/$everything. Стандартные поля попадают в standard aggregations; custom extensions требуют от consumer-а знания «откуда вытаскивать».
То есть extensions — это trade-off между удобством на нашей стороне и понятностью на стороне consumer-а. Решение про их минимизацию зависит от того насколько мы хотим быть понятными для downstream. До тех пор пока единственный consumer — это мы сами (B2C-приложение BloodGPT), extensions можно добавлять свободно. Когда появляется второй consumer (партнёр / EHR-интеграция) — каждый extension становится payload-ом «прочитайте docs»; стандартные поля — нет.
Подробнее про interop-механику и роль meta.profile — fhir-profiling.
Ильдар: «isHealthy/parameterDetails → Extensions на Observation. а isHealthy может быть стандартный interpretation в observation» (триггер пересмотра)
Claude (после пересмотра): «Получается ноль custom extensions — всё ложится на стандартный FHIR R4»
Текущее состояние (2026-05): extensions > 1, цель — минимум
Формулировка «ровно один extension» устарела. По мере роста pipeline (V2.5 rich-output, recognition→FHIR, follow-up tiers, trends, patient-summary) набор custom extensions расширился. На 2026-05 в коде используются как минимум:
requires-doctor-preparation— Composition; нужна ли подготовка перед визитом. Стандартного аналога нет.bloodgpt-parameter-analysis— ClinicalImpression, repeating; V2.5 per-parameter rich output (clinicalInterpretation,whatAdditionalDataWouldClarify, и т.д.). Стандартного места для structured per-param AI-разбора нет.source-diagnostic-report— Composition (patient-summary); на данных какого DiagnosticReport собрано summary.original-name— Observation; имя параметра как в документе до нормализации.extraction-source— Observation/др.; откуда извлечён ресурс при распознавании (легче, чем полноценный Provenance на каждый Observation).trending-group-id— Observation; группировка биомаркеров в один тренд.urgent-test-tier— CarePlan.activity / ServiceRequest; tier срочности досдачи.activity-priority— CarePlan.activity; приоритет (кандидат на замену стандартнымServiceRequest.priority).current-symptoms,health-status— self-reported данные пациента; место в стандартном ресурсе под вопросом.
Плюс две URL-конвенции одновременно: http://bloodgpt.com/fhir/StructureDefinition/... и http://bloodgpt.com/fhir/extension/....
Действующий принцип, а не запрет:
- Сначала ищем стандартную R4-конструкцию (
Observation.interpretation/.note[]/.referenceRange[],Composition.sectionс customsection-typecode,CarePlan.activityсkind=ServiceRequest,Provenance). Customsection-typeкоды в собственном CodeSystem — НЕ считаются extension’ом. - Extension — когда стандартного места нет (
requires-doctor-preparation,bloodgpt-parameter-analysis) или когда стандартный ресурс был бы непропорционально тяжёл. - Каждый существующий extension — кандидат на ревизию: убрать / заменить стандартом / схлопнуть.
Carry-over: полная инвентаризация extensions в коде (grep StructureDefinition/, grep fhir/extension/), по каждому — решение оставить/заменить/схлопнуть; унифицировать две URL-конвенции в одну.
Следствия
- В кодовой базе:
fhir-composition-builder.tsиfhir-careplan-builder.tsстроят resources преимущественно на стандартном R4; custom extensions ограничены и каждый явно оправдан (см. «Текущее состояние»). - AI enrichment (text-комментарии + interpretation H/L/N) добавляется отдельным шагом после save (см. ai-enrichment-separate-step) через
Observation.note[]иObservation.interpretation. - Внешние FHIR-клиенты (включая будущие интеграции с госпитальными EHR) могут читать наши данные стандартным FHIR-парсером.
- При появлении новых AI-полей — сначала ищем стандартное FHIR-место, extension только как последний резорт. (Пример 2026-05: per-observation lab-комментарии (
inline_interpretationиз распознавания) →Observation.note[], conditional-range блоки →Observation.referenceRange[].text— без extension. См. recognize-per-observation-comments.) - LLM-summary
/home/i/JOBS/BloodGPT/specs/decisions/023-fhir-ai-content-architecture.md(frontmatter “Принято: Вариант C”) — не canonical ADR, а LLM-эксперимент; tactical confirmation Variant C сделан Ильдаром в начале session 065369a0 (2026-03-14, “давай пока оставим вариант C”) и через реализацию в PR #158.
Связано
- fhir-composition — наш контейнер для AI overview
- fhir-careplan — follow-up рекомендации
- fhir-observation — параметры с interpretation + note
- fhir-annotation — Annotation как data type для note[]
- fhir-profiling — общая механика FHIR-профилирования и graceful degradation
- phi-in-fhir-not-sql — связанное решение про FHIR как source-of-truth — active
- ai-enrichment-separate-step — следствие (когда добавлять note[] и interpretation)
- authorship-organization-not-device — кого ставить в author
- formalize-fhir-profiles — если решим формализовать profile — что станет с extensions — draft
- extension-url-conventions — sub-вопрос: какой URL pattern использовать для самих extensions — draft
Источники
Сноски
-
HL7 v3 ObservationInterpretation ValueSet, accessed 2026-05-17, https://terminology.hl7.org/CodeSystem-v3-ObservationInterpretation.html. ↩
-
FHIR R4 Composition, accessed 2026-05-17, https://hl7.org/fhir/R4/composition.html. ↩
-
FHIR R4 CarePlan, accessed 2026-05-17, https://hl7.org/fhir/R4/careplan.html. ↩