Реализовано в chain 6bfe41d1 (2026-05-10): save-fhir-ai-resources ветвится по scope (см. «Реализация»). Решение про как хранить один (POST новой каждый run vs PUT canonical) — отдельно, см. health-report-versioning-model; здесь — про имя, scope и где живёт указатель.

Контекст

«Текущий взгляд системы на здоровье пациента» (нарративный обзор: identifiedPatterns, healthConsiderations, recommendedSteps, panelOverviews, trends) — одна сущность, но в коде она носит 5-6 разных имён:

  • Composition.type.coding.code = "test-overview" (fhir-composition-builder.ts), Composition.type.coding.display + Composition.title = "Blood Test Overview"
  • Composition.identifier.system = "http://bloodgpt.com/fhir/identifier/patient-summary" (save-fhir-ai-resources.function.ts, константа PATIENT_SUMMARY_IDENTIFIER_SYSTEM)
  • Postgres-колонка BloodTest.fhirAssessmentId (comment: «Composition/{id} — AI assessment (overview, patterns, trends)») — слово «Assessment», хотя это не RiskAssessment
  • Inngest pipeline patient-summary-pipeline (event analysis/patient-summary.requested)
  • Enrichment-функция slug test-overview-generation → eval-маппинг test_overview
  • Эндпоинты: b2b POST /api/v1/summary {patient_id}; b2c /api/summary и /api/patient-summary (две штуки); компонент PatientHealthSummary.tsx
  • Prisma Patient.lastSummary* (denorm status-поля)

То есть «test-overview» / «Blood Test Overview» / «patient-summary» / «Assessment» / «summary» / «patient health summary» — несогласованно. title = "Blood Test Overview" вдобавок врёт: ресурс patient-scoped (одна Composition на пациента, обновляется при каждом тесте — см. interpretation-scope-patient-vs-test, health-report-versioning-model), а не «обзор одного теста».

FHIR resourceType — Composition (стандартный R4 «клинический документ»; не RiskAssessment, несмотря на имя колонки fhirAssessmentId). Выбор «Composition как контейнер» зафиксирован — см. zero-extensions-fhir и health-report-versioning-model; здесь не пересматривается.

Рассматривали (имя)

  • «Patient Summary»identifier.system уже это говорит; короткое, не привязано к «тесту», совпадает с industry-термином (IPS = International Patient Summary, см. patient-summary).
  • «Patient Health Summary» / «Health Summary» — длиннее; «health» избыточно.
  • Оставить «Test Overview» — отвергнуто: ресурс не тест-scoped.

Выбрали (Option B): расщепить по scope, оба ресурса валидны

Не «переименовать один Composition», а два разных Composition по scope — потому что у нас два interpret-трека и они делают разные вещи:

  • V2.5 patient-scoped трек (patientSummaryPipeline, b2c «Update analysis» → /api/summary) → «Patient Summary»: Composition.type.code=patient-summary, title="Patient Summary", identifier=…/patient-summary|{fhirPatientId}. Один на пациента, upsert по identifier (versions через FHIR _history).
  • Легаси test-scoped трек (analysisPipeline, POST /api/v1/interpret/:testId — путь b2b-клиентской интеграции, не мёртвый) → «Blood Test Overview»: Composition.type.code=test-overview, title="Blood Test Overview", identifier=…/test-id|{testId}. Один на тест, upsert по testId. BloodTest.fhirAssessmentId указывает на этот test-scoped Composition — и имя Assessment тут как раз ОК (это test assessment).

CarePlan («Follow-up Recommendations») — зеркально: patient-scoped (V2.5) vs test-scoped (legacy), по тому же identifier-правилу. CarePlan не имеет type.code — имя одно, различает identifier.

Безопасно: никто не диспатчит по Composition.type.code (проверено grep’ом — test-overview встречался только в builder’е и в маппинге slug’а enrichment-функции test-overview-generation, не в ресурсе). Старые Composition’ы в FHIR (pre-Option-B) — гибрид: type=test-overview + identifier=…/patient-summary|{patientId}; первый V2.5-прогон после деплоя апдейтит их на type=patient-summary (PUT по тому же identifier).

patient-summary-pipeline (Inngest) / test-overview-generation (enrichment slug) — имена не трогаем.

Реализация (chain 6bfe41d1, 2026-05-10)

scope ("patient" | "test", default "patient") прокидывается: interpret-pipeline → 5× enrichment/*.requested (interpretation-analysis.function.ts ставит scope:"test"; patient-summary.function.tsscope:"patient") → enrichment factory (factory.ts читает) → checkAndTriggerPdf (helpers.ts) → событие fhir/ai-resources.savesave-fhir-ai-resources.function.ts. Там isPatientScoped = scope !== "test": если patient-scoped — передаём patientId в builder’ы (→ patient-scoped Composition/CarePlan) и upsert по patient-summary|{patientId}; если test-scoped — НЕ передаём patientId (→ test-scoped, builder’ы уже умеют это когда patientId undefined) и upsert по test-id|{testId}. fhir-composition-builder.ts: type.code/title = patient-summary/«Patient Summary» когда patientId задан, иначе test-overview/«Blood Test Overview». Файлы: interpretation-analysis.function.ts, patient-summary.function.ts, factory.ts, helpers.ts, save-fhir-ai-resources.function.ts, fhir-composition-builder.ts, schema.prisma (комментарий), b2c-dashboard/app/api/patient-summary/route.ts (fallback теперь пропускает test-scoped Composition’ы). typecheck чистый (analysis-core / analysis-worker / b2c-dashboard).

Где живёт указатель / как читать

  • Patient Summary — резолвится по Composition?identifier=…/patient-summary|{fhirPatientId}. Это делает b2c /api/patient-summary (+ _sort=-_lastUpdated fallback, который теперь отфильтровывает test-scoped Composition’ы).
  • BloodTest.fhirAssessmentId — для легаси-трека = его test-scoped Composition; для V2.5-трека = тоже пишется (= patient Composition id, для back-compat легаси-читателей). Легаси-читатели (/api/v0/..., patient-portal, b2b-platform, fhir-test-data-fetcher, doctor-review-gate) — продолжают читать fhirAssessmentId, не трогаем.

Следствия

  • Внешний FHIR-клиент видит у пациентского summary Composition.type=patient-summary / title="Patient Summary" — не вводит в заблуждение «Blood Test Overview».
  • Пациент, прошедший оба трека: N test-scoped Composition (по одной на легаси-интерпретированный тест) + 1 patient-scoped. На практике у большинства — 1 patient-scoped и 0 test-scoped (test-scoped появляются только через b2b-клиентский /api/v1/interpret/:testId).
  • Свежезагруженный тест — без Composition, пока не запущен interpret (legacy) / summary (V2.5).
  • Та же naming-болезнь остаётся у BloodTest.fhirDocumentCompositionId (source-document envelope Composition) — known follow-up.

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

  • Migration старых гибридных Composition’ов (type=test-overview + identifier=…/patient-summary): полагаемся на «следующий V2.5-прогон апдейтит type» или одноразовый bulk-script? (drop dev / bulk stage / leave prod)
  • /api/summary vs /api/patient-summary (b2c) — какой остаётся канонным reader’ом, второй → редирект/удалить.
  • fhirDocumentCompositionId — отдельная decision или сюда расширением.
  • Multi-tenant: identifier per (patient, organization) или global per patient — пересекается с health-report-versioning-model.

Связано

Источники