FHIR версионирует каждый resource «из коробки»: создание + каждое изменение порождает новую запись, прошлые версии остаются доступны через _history API. BloodGPT использует это для всех evolving entities — derived AI ресурсов (Composition / CarePlan / ClinicalImpression), которые перезаписываются на каждый analysis run. Эта страница — про механизм; app-level правила (как выбирать default-render из истории, как пэйрить связанные ресурсы) — в health-report-versioning-model.
meta.versionId и meta.lastUpdated
Каждая версия resource’а на сервере несёт server-assigned поля в meta:
meta.versionId— идентификатор версии. Free-form string по спеке; на практике HAPI и Google Healthcare API используют integer ("1","2", …), но клиент не должен это assumить.meta.lastUpdated— server timestamp последней записи этой версии. Стабильно отражает «когда».meta.tag/meta.security/meta.profile— структурированные labels (см. meta-блок).
Клиент никогда не назначает versionId / lastUpdated сам. Любые значения, отправленные в этих полях, сервер игнорит и перезаписывает. Это даёт строгий monotonic ordering и server-authoritative timestamp.
Conditional PUT по identifier — основной механизм upsert’а
Когда домен говорит «одна сущность на пациента, обновляется», canonical FHIR-паттерн —conditional PUT по бизнес-identifier’у, не POST.
PUT /Composition?identifier=http://bloodgpt.com/fhir/identifier/patient-summary|Patient-2269
Content-Type: application/fhir+json
{ "resourceType": "Composition", "identifier": [...], ... }Семантика:
- Сервер ищет resource по
identifier=system|value. - Если нет совпадения → POST-эквивалент: создаёт resource с новым id,
versionId=1. - Если одно совпадение → PUT-эквивалент: обновляет тот же resource id,
versionId++, прошлая версия остаётся в_history. - Если несколько совпадений → 412 Precondition Failed (idempotency violation — duplicate canonical pointers).
Альтернатива — PUT /Composition/{id} (по resource id), но это требует от клиента уже знать id. Conditional PUT по identifier работает с первого создания и далее, единый writer-code-path.
_history API — как читать прошлые версии
Стандартные FHIR-эндпоинты:
GET {Resource}/{id}/_history— bundle всех версий resource’а в обратном хронологическом порядке (newest first). Каждаяentry— полная resource на момент той версии.GET {Resource}/{id}/_history/{versionId}— точная версия.GET {Resource}/{id}/_history?_count=N&_since=YYYY-MM-DD— пагинация и фильтр поlastUpdated.
Bundle entry’и несут request.method (POST/PUT/DELETE) — можно отличить версии created от updates от тombstone’ов (для удалённых ресурсов _history тоже работает; entry с request.method=DELETE маркирует deletion).
В отличие от обычного GET /{Resource}/{id} (возвращает latest), _history-чтения не платят за индекс по lastUpdated — версии физически линкованы внутри resource record’а.
Versioned vs unversioned references
Reference в FHIR может быть pinned к конкретной версии:
{ "reference": "Patient/2269/_history/5" }— versioned reference, всегда возвращает ту же версию даже после следующих PUT’ов. Без /_history/{n} — unversioned, всегда возвращает latest. См. Ссылки в fhir-basics.
Versioned references ценны для immutable audit trail (e.g. Provenance.entity.what), но клиенту нужно их явно генерировать.
HAPI-specific: meta.tag accumulates additively через conditional-PUT
Важный quirk, на который BloodGPT полагается в CI ↔ Composition pairing.
По спеке meta.tag — это набор labels, и каждый PUT может его перезаписывать. HAPI вместо replacement делает union: tags, присутствовавшие в любой прошлой версии, переносятся в новую версию. Effect: версия, созданная в run’е N, несёт tags из run’ов 1..N (если каждый run писал свой run-id тэгом).
Следствие — можно пэйрить два ресурса (Composition и CI), записанных в один и тот же run, через tag-set superset: «paired CI = старейшая версия CI, у которой tag-set ⊇ Composition’s tag-set». Это работает потому что:
- Каждый run стампит tag
analysis-run|{runId}на каждый ресурс, который пишет. - HAPI на conditional-PUT накапливает тэги.
- Версии, написанные в run K, содержат
{runId-1, ..., runId-K}— все предыдущие плюс current. - «Старейшая CI с superset Composition’s tags» = CI version, написанная в том же run’е (либо первая, которая накопила всё что у Composition).
Если HAPI бы делал replace (как формально по спеке допустимо), meta.tag каждой версии бы содержал только текущий run-id — pairing работал бы через прямой equality, не superset. Наш writer-code независимо от поведения сервера стампит только current-run tag; superset-logic — на read-side как defense против любого варианта server behavior.
Это не FHIR-стандарт, а HAPI-implementation choice. Google Healthcare API (production) — поведение надо проверять отдельно при portability concerns.
Provider support
| Provider | conditional PUT | _history API | meta.tag accumulation |
|---|---|---|---|
| HAPI (dev / staging) | ✓ | ✓ | accumulates |
| Google Healthcare API (prod) | ✓ | ✓ | TBD verify |
Конкретные тесты production-поведения нужно сделать при движении из stage в prod (особенно последняя колонка — на ней висит CI-pairing).
Failure modes
- Multiple matches при conditional PUT.
412 Precondition Failed. Сигнализирует поломку identifier-scheme’а (где-то писали duplicates). Recovery — мануальный cleanup. If-Matchmismatch. Если writer добавляетIf-Match: W/"{versionId}"(optimistic locking) и сервер увидел уже более свежую версию —412. Используется для race-condition защиты; BloodGPT сейчас не использует, полагается на Inngest dedup.- Stale read. Между
_historywalk и follow-up read latest может появиться новая версия. Это не баг, это eventual consistency — для read-only UI обычно ОК; для writer’ов требуетIf-Match. - Capped
_history. Некоторые серверы конфигурируют retention period или max version count. HAPI по умолчанию — unlimited; production limits надо проверять.
Открытые вопросы
- Production tag-accumulation behavior на Google Healthcare API — наш CI-pairing зависит от additive-tags-через-PUT. Если prod replaces — pairing деградирует на lastUpdated-fallback. Нужен test.
If-Matchadoption для writer’ов с риском параллельных run’ов — currently отложено.
Связано
- fhir-basics —
meta, references, базовая модель - fhir-resource-origin-and-lifecycle — origin → mutability rules;
_historyper-origin - fhir-meta-tagging — наша tag scheme (origin, analysis-run)
- health-report-versioning-model — как BloodGPT применяет этот механизм к Health Report ресурсам
- fhir-meta-tagging — application-side tag conventions