В нашем FHIR-store сосуществуют ресурсы с разной природой — пациент загрузил PDF лабы, наш AI-pipeline сгенерировал интерпретацию, в будущем приедут API-imports от лабораторий-партнёров. Сейчас активно используются два класса (user-uploaded, ai-generated), третий (external-data) ожидается; могут появиться и другие по мере расширения источников. Каждый origin несёт своё lifecycle expectation — допустимо ли (и при каких условиях) переписывать ресурс после того, как он попал в FHIR-store. Тип источника записывается в meta.tag ресурса; механизм этой пометки — fhir-meta-tagging. Эта страница про rules: что значит для каждого origin «можно перезаписывать», «нельзя», «можно с ограничениями».
Lifecycle rules
| Origin | Lifecycle ожидание | Почему |
|---|---|---|
user-uploaded | Immutable — наш pipeline не делает PUT после создания | Это raw input от пациента / лаба; перезапись = искажение source-of-truth. |
external-data | (future) Immutable | То же rationale — raw input от API-партнёров. |
ai-generated | Mutable — PUT-обновления норма | Пере-интерпретация на каждом analysis run; новая модель / новые данные → новая интерпретация поверх существующей. |
Что значит «immutable» здесь. FHIR-сервер при PUT всегда создаёт новую версию (meta.versionId инкрементируется, предыдущая остаётся в history) — это его внутренний механизм аудита, и immutable не запрещает его технически. Наше правило — наш pipeline сам не вызывает PUT на ресурсах с user-uploaded / external-data origin. Versioning через versionId мы не используем (нет UI, который бы показывал старые версии); это availability, не часть нашей модели данных.
А как тогда обновлять lifecycle-state ресурса (например, перевести MedicationStatement из active в inactive, отозвать AllergyIntolerance)? У ресурсов есть собственные status-поля (MedicationStatement.status, Condition.clinicalStatus, и т.п.) — их смысл «текущее клиническое состояние сущности», и переходы там часть нормальной FHIR-семантики. Это не lifecycle origin-ресурса: tag говорит «откуда пришли данные», status-поле говорит «как сейчас обстоит дело клинически». Когда пациент сообщает «уже не принимаю» — это создание нового MedicationStatement или update существующего по обычным FHIR-правилам, не нарушение immutability по origin (за этим выбором — отдельный design вопрос, не часть этой страницы).
Stale-detection — основной use case lifecycle
«У пациента появились новые данные с прошлого анализа» = newer raw resource:
Observation?subject=Patient/X
&_tag=origin|user-uploaded
&_lastUpdated=gt<composition.lastUpdated>
Только user-uploaded → false-positive не возможен (наши AI-обновления имеют другой tag, поэтому не trigger’ят stale). См. health-report-versioning-model про лифциклу нашей aggregate Composition.
Gap: enrichment ломает immutability raw Observation
Сейчас enrich-fhir-observations.function.ts делает PUT Observation/{id} для добавления note[] (AI commentary) + interpretation (H/L/N от AI) на user-uploaded Observation. Это нарушает правило «user-uploaded immutable».
Подтверждено эмпирически: Maria’s Observation/53 имеет meta.versionId: 7, issued: None (поле дропнуто на enrichment PUT — bug, не by design).
Параллельно ClinicalImpression per-биомаркер (ClinicalImpression.extension[bloodgpt-parameter-analysis]) уже несёт ту же AI-интерпретацию в богаче shape, как отдельный ресурс с origin: ai-generated. То есть enrich-fhir-observations — legacy step, дублирует функционально то что делает ClinicalImpression, но в худшем формате: нарушает immutability user-uploaded, теряет поля при PUT (issued дропается), shape поверхностный. По состоянию на 2026-05-18 step ещё живёт в apps/analysis-worker/src/inngest/functions/enrichment/enrich-fhir-observations.function.ts — не удалён.
Направление: step удаляется, читатели интерпретации переключаются на ClinicalImpression, Observation остаётся как raw value-with-range без последующих PUT, issued перестаёт страдать. Координация с Vlad (см. recognize-per-observation-comments про Observation.note[] reservation — совместимо).
См. recognize-per-observation-comments — там зафиксировано что Observation.note[] зарезервирован под лаб-аннотации (inline_interpretation / footnotes из документа, author = лаба), AI контент туда не пишется. Удаление enrich-fhir-observations совместимо.
Discharge summary case
Пациент приносит выписку другой клиники (PDF). Recognition распознаёт её как clinical document → создаёт Composition с meta.tag origin: user-uploaded. Наша patient summary при PUT остаётся origin: ai-generated.
Lookup на dashboard ?_tag=ai-generated&type=Composition находит только нашу — пациентская выписка не вмешивается. Не нужны костыли с identifier-сопоставлением. Это иллюстрирует зачем origin tagging нужен — resource type сам по себе не отличает; разделение делается на уровне meta.tag (см. fhir-meta-tagging про механизм).
Migration policy для существующих данных
Tagging внедряется postfactum — существующие ресурсы без tag. Варианты:
- Drop dev data — единичная очистка, проще всего
- Bulk-tag prod data через скрипт — все ресурсы старше X получают
origin: user-uploaded(assumption: до deploy AI не писал в этот FHIR store, всё что было — пациент загружал) - Помечать с момента deploy — старые ресурсы без tag implicitly user-uploaded (по дате создания до деплоя)
TBD — выбрать политику когда дойдёт до implementation.
Открытые вопросы
- Migration policy для существующих данных — drop dev / bulk-tag stage / leave prod as-is until deploy?
enrich-fhir-observationsretirement — координация с Vlad (BG-1337 stage-2)? Скоп ~1 день после удаления и переключения читателей на ClinicalImpression.- External-data lifecycle — когда появятся API-imports от лаб-партнёров / Apple Health, validate immutability assumption (вдруг партнёр re-sends corrected values?).
- Composition.status semantics —
Composition.status=completedсейчас выставляется после recognition, до AI-интерпретации; это про status-поле, не про origin-lifecycle. Отдельный design (стоит ли промежуточныйpreliminary, или AI-output живёт в отдельном Composition / ClinicalImpression) — не часть этой страницы.
Связано
- fhir-meta-tagging — tagging-механизм (origin / source / security / Provenance). Эта страница про lifecycle консумирует tag как input.
- health-report-versioning-model — lifecycle конкретного aggregate ресурса (PUT canonical Composition по identifier); use case stale-detection
- ai-enrichment-separate-step — текущее legacy решение про enrichment-PUT-step, которое эта decision рекомендует удалить
- zero-extensions-fhir — общий принцип «использовать стандартные FHIR-механизмы вместо extensions»; релевантно для composition-status вопроса
- authorship-organization-not-device — author-attribution, тоже про «кто это создал» (но конкретно про agent identity, не lifecycle)
- recognize-per-observation-comments — раздел
Observation.note[]только под лаб-контент — совместимо с удалением enrich-fhir-observations