Реализовано в 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(eventanalysis/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.ts — scope:"patient") → enrichment factory (factory.ts читает) → checkAndTriggerPdf (helpers.ts) → событие fhir/ai-resources.save → save-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=-_lastUpdatedfallback, который теперь отфильтровывает 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/summaryvs/api/patient-summary(b2c) — какой остаётся канонным reader’ом, второй → редирект/удалить.fhirDocumentCompositionId— отдельная decision или сюда расширением.- Multi-tenant:
identifierper (patient, organization) или global per patient — пересекается с health-report-versioning-model.
Связано
- health-report-versioning-model — как хранить (POST каждый run vs PUT canonical); identifier-scheme
…/patient-summary - interpretation-scope-patient-vs-test — почему patient-scoped + паттерн «два трека»
- zero-extensions-fhir — Composition как стандартный R4-контейнер для AI overview
- patient-summary — концептуальная модель (IPS как база, retrieval)
- fhir-composition, fhir-careplan
- staged-output-fhir-storage — общий принцип FHIR-storage