Какие URL’ы класть в Resource.identifier.system на ресурсах, которые пишет Health Report Pipeline. Сейчас не зафиксировано в один принцип — три URL’а живут по разной логике, и при rename’е вocabulary возникают вопросы куда что переименовывать.

Контекст

В FHIR Resource.identifier[] — массив business-identifiers (отдельно от Resource.id, который generator’ит сервер). Каждый identifier = { system: URL, value: string }. system namespacing’ует identifier (определяет «какого рода» он), value — само значение.

Мы используем identifier’ы для:

  • Поиска ресурса по бизнес-ключу: GET /ClinicalImpression?identifier=<system>|<value>
  • Conditional PUT (upsert): PUT /ClinicalImpression?identifier=<system>|<value> — если уже есть, апдейт; нет — создание

Это позволяет нам не хранить FHIR-server-assigned id для look-up’a — мы знаем fhirPatientId и identifier system из codebase, и этого хватает.

Что у нас сейчас (Health Report Pipeline)

Pipeline пишет три patient-scoped FHIR-ресурса. Все они keyed по fhirPatientId (один resource per patient):

Resourceidentifier.systemidentifier.value
ClinicalImpressionhttp://bloodgpt.com/fhir/identifier/clinical-impression<fhirPatientId>
Compositionhttp://bloodgpt.com/fhir/identifier/patient-summary<fhirPatientId>
CarePlanhttp://bloodgpt.com/fhir/identifier/care-plan<fhirPatientId>

Inconsistency: два URL’а названы по resource-type (clinical-impression, care-plan), один — по семантической роли (patient-summary). Vocabulary evolves («patient-summary» → «health-report-composition»? или «report-composition»?), и неясно, переименовывать ли все три по одному правилу.

Возможные принципы

Принцип A — по resource-type

system = resource type, всегда. Семантическая роль (что этот resource значит) — отдельно (через Composition.type code, и т.п.), не через identifier.system.

http://bloodgpt.com/fhir/identifier/clinical-impression
http://bloodgpt.com/fhir/identifier/composition
http://bloodgpt.com/fhir/identifier/care-plan

Pro:

  • Стабильно — vocabulary меняется, identifier URL’ы — нет
  • Однородно — один паттерн на все resource types
  • Существующие два из трёх уже соответствуют (clinical-impression, care-plan)

Con:

  • Не различает множественные writer’ы. Если test-analysis-pipeline тоже пишет Composition этому пациенту — конфликт identifier’a. (На сейчас этого нет, но архитектурно ограничивает.)
  • Менее explicit что resource — Health Report’овский

Принцип B — по семантической роли

system = роль в продукте.

http://bloodgpt.com/fhir/identifier/health-report-clinical-impression
http://bloodgpt.com/fhir/identifier/health-report-composition
http://bloodgpt.com/fhir/identifier/health-report-care-plan

Или короче:

http://bloodgpt.com/fhir/identifier/health-report/clinical-impression
http://bloodgpt.com/fhir/identifier/health-report/composition
http://bloodgpt.com/fhir/identifier/health-report/care-plan

Pro:

  • Explicit что resource написан Health Report Pipeline’ом
  • Можно иметь несколько Composition на пациента из разных pipeline’ов без коллизии identifier’a
  • Read-side понимает context («это Health Report’овский Composition, не lab discharge-summary»)

Con:

  • Vocabulary-bound — если pipeline переименуется (как уже было actuality-pipeline-v1health-report), identifier URL’ы превратятся в legacy
  • Длиннее
  • Дублирует с meta.tag (где можно положить pipeline-source)

Принцип C — по resource-type + meta.tag для source

system = resource-type (Принцип A), а Resource.meta.tag несёт «кто написал». Read-side фильтрует по meta.tag когда нужно ограничить scope.

identifier.system = http://bloodgpt.com/fhir/identifier/clinical-impression
meta.tag[].system = http://bloodgpt.com/fhir/source
meta.tag[].code   = health-report-pipeline

Pro:

  • Identifier стабилен (Принцип A)
  • Source трекается separately
  • Existing AIAST security tag, analysisRunId tag — уже используем meta.tag pattern
  • FHIR-conventional (meta.tag = «labels» что-то)

Con:

  • Read-side сложнее (нужен _security / _tag filter в запросах)
  • Если несколько pipeline’ов пишут в один CI — overwrite друг друга

Граница между identifier и meta.tag

Они часто путаются — оба key-value на ресурсе, оба имеют system + value/code, оба searchable. Их различает роль:

identifier[]meta.tag[]
Семантика«WHICH resource» — business primary key«WHAT property has this resource» — label/classification
Cardinalityобычно один per (system, value) — уникальная пара идентифицирует ресурсmany per resource, обычно multiple одновременно
Stabilitypermanent — заданный identifier не меняетсяaccumulates — теги добавляются / некоторые могут rotate
Search semantics?identifier=system|value — exact match, retrieve THE resource?_tag=system|code — filter, retrieve set of resources
Use caseconditional PUT / upsert / lookup by patientfilter «все AI», «всё из chat», «всё в этом run»
Наша конвенцияresource-type per patient (см. выше) — один CI / Composition / CarePlan per patientorigin / source / security / run-id (см. fhir-meta-tagging)

Примеры в нашей системе:

ClinicalImpression для пациента 52:

{
  "identifier": [
    { "system": ".../identifier/clinical-impression", "value": "52" }     // ← КТО этот ресурс
  ],
  "meta": {
    "tag": [
      { "system": ".../CodeSystem/origin",       "code": "ai-generated" },     // ← origin
      { "system": ".../CodeSystem/source",       "code": "document-import" },  // ← source
      { "system": ".../analysisRunId",           "code": "<runId>" }           // ← run-stamp
    ],
    "security": [
      { "system": "...v3-ObservationValue", "code": "AIAST" }                  // ← AI-asserted
    ]
  }
}

identifier«это THE CI для патиента 52» (упсёрт ключ, обновляется через PUT). meta.tag + meta.security«вот какие свойства у него» (labels).

Боковой случай — когда выбор не очевиден

Source attribution (meta.tag source = health-chat / survey / document-import) могла бы быть identifier-system’ом (identifier.system=health-chat, value=...). Не работает потому что:

  • Один Composition с identifier’ом «document-import|» можно было бы сделать, но тогда identifier-key — это сам документ, а не «THE Composition пациента». Conditional PUT по identifier перезаписывал бы wrong resource.
  • Если на patient’е есть 3 chat-сессии + 1 survey + 2 document-import’a, и все пишут Composition’ы — multiple identifier values нужны (один CI per chat-session). Дублирует resourceCounts от хвоста UI.

source — это категория, не identity. → meta.tag, не identifier.

Per Принцип A (resource-type), source как идентификатор был бы overengineering. См. fhir-meta-tagging для полной policy.

Параллельные вопросы (открытые отдельно)

Помимо identifier.system есть ещё несколько persisted strings, у которых аналогичная проблема:

  • Composition.type.code — сейчас patient-summary (по семантической роли). Vocabulary-bound. Решено в patient-summary-composition-naming (active) — оставить как «patient-summary» для V2.5-track. Переоткрыть когда фиксируем Health Report имя?
  • FHIR extension URL bloodgpt-parameter-analysis — на CI sub-extension’ах. Сейчас в fhir-clinical-impression-builder.ts:227 и fhir-clinical-impression-parser.ts:13. Vocabulary-bound (тоже про parameter vs biomarker).
  • Device.idbloodgpt-pipeline-v2-5. Version-bound. Через v2-5 хвост.
  • DB columnsBloodTest.fhirAssessmentId, Patient.lastSummaryStatus / lastSummaryAt / lastSummaryRunKey. Vocabulary-bound.

Каждый можно решать по тому же principle-choice как identifier.system. Согласованный outcome желателен — иначе один identifier меняется, другой остаётся, и snake-pit получается ещё хуже.

Что blокирует rename — existing data

На сейчас staging HAPI содержит resources с текущими identifier’ами. Если rename’нем без migration:

  • Existing CIs с identifier.system=clinical-impression не найдутся по новому URL’у
  • Conditional PUT под новым URL создаст orphan (старый + новый)

Migration варианты:

  1. Wipe & re-seed staging FHIR — если staging-data не precious
  2. Read-side fallback — поддерживать оба URL’а в reader’е, новые writes под новым URL, transition period
  3. Backfill script — переписать identifier.system на existing resources

Per Phase F rationale: actuality-mvp-dirty branch не deploy’ится в prod; staging-data написана старой production-line.

Открытые вопросы

  • Какой principle выбрать (A / B / C)?
  • Какие persisted strings включать в один coherent rename (extension URL, Composition.type, Device.id, DB columns)?
  • Wipeable ли staging FHIR data — определяет нужна ли migration
  • Будут ли несколько writer-pipeline’ов на одном resource (если да — Принцип B/C предпочтительнее)
  • Cohabitation с FHIR R4 standard identifiers (MRN, SSN system’ы) — конвенция HL7 vs наша

Связано

Источники