English | Русский
Clinician-in-the-loop ECG second-opinion workflow system.
A standalone TypeScript API that orchestrates the full lifecycle of an ECG second-opinion case — from intake and quality checks through AI-assisted draft generation to mandatory clinician review, finalization, and delivery.
⚠️ Research Use Only. This repository is not FDA-cleared, not CE-marked, and not approved for clinical use. It must not be used for clinical decision-making. Every output requires review by a qualified clinician.
This repository is not an autonomous diagnostic system. It is a clinician-in-the-loop workflow layer that enforces human review before finalization or delivery.
- What This Project Does
- How It Works
- Current Baseline
- Architecture
- Technology Stack
- API Surface
- Security
- Getting Started
- Testing
- Project Structure
- Diagnostic Categories
- Safety Policy
- Scientific Foundation
- Regulatory Positioning
- Roadmap
- Community
- License
- Русская версия
ECG Second Opinion is not an "AI that reads ECGs." It is the workflow system around the AI — the control plane that ensures every ECG case follows a strict, auditable path from submission to delivery.
Think of it as a transparent orchestrator:
- A clinician or integration system submits a 12-lead ECG recording with patient alias and recording metadata
- The system queues the recording for AI analysis (1D-CNN classifier)
- An inference worker processes the recording — runs classification, measures confidence, computes uncertainty metrics
- The system captures the AI output as a structured draft report — never as a final diagnosis
- A clinical safety policy evaluates the result and raises flags for low-confidence, high-risk, or disagreement scenarios
- A human clinician (cardiologist, electrophysiologist) must review the draft, add their impression, and explicitly approve or modify it
- Only after clinician approval does the case move to finalization and delivery
- Every step is logged, timestamped, and traceable via an append-only audit trail
The key idea: The AI generates a draft. A human makes the decision. The system enforces this boundary in code.
Submitted → InferencePending → AwaitingReview → Reviewed → Finalized
↓
InferenceFailed
Every arrow is a guarded transition. The aggregate rejects invalid state changes with typed domain errors.
| Transition | Guard |
|---|---|
| Submitted → InferencePending | Case just created, queued for async inference |
| InferencePending → AwaitingReview | Inference completed, safety policy evaluated |
| InferencePending → InferenceFailed | Worker error captured, case not stuck |
| AwaitingReview → Reviewed | Clinician submitted review decision |
| Reviewed → Finalized | Clinician approved; no unresolved blocking safety flags |
ECG Recording ──→ Metadata Validation
│
▼
Inference Worker (async)
┌────────┴─────────┐
│ 1D-CNN (planned) │
│ Metadata fallback│ ← current
└────────┬─────────┘
│
▼
Safety Policy Evaluation
(8 clinical rules, AHA/ACC aligned)
│
▼
AwaitingReview + Safety Flags
- FHIR R4 DiagnosticReport — structured export with contained Observations, performer references, and Research-Use-Only extension
- Structured ECG Report — internal JSON format with findings, recommendations, limitations, and safety flags
What is implemented (backed by tests and running code):
| Component | Status | Evidence |
|---|---|---|
| 6-state case aggregate | ✅ Complete | 25 unit tests |
| 3-tier auth (operator API key, reviewer JWT HS256, internal bearer) | ✅ Complete | API integration tests incl. malformed, issuer, audience, role, signature, claims, timing, and token rejection cases |
| Zod input validation (all endpoints) | ✅ Complete | 12 validation tests |
| Clinical safety policy (8 rules) | ✅ Complete | 13 safety policy tests |
| Metadata-fallback inference service | ✅ Stub | @sota-stub tagged |
| FHIR R4 DiagnosticReport export | ✅ Complete | API integration tests |
| Structured ECG report builder | ✅ Complete | API integration tests |
| Prometheus metrics (7 instruments) | ✅ Complete | Wired in routes |
Health probes (/healthz, /readyz) |
✅ Complete | API integration tests |
| Correlation ID propagation | ✅ Complete | Header tests |
| Fail-fast config validation | ✅ Complete | Config test suite incl. reviewer audience guard |
| Append-only audit trail | ✅ Complete | Audit trail test |
| Async inference with failure recovery | ✅ Complete | InferenceFailed state tests |
| Safety flag resolution by reviewer | ✅ Complete | API + unit tests incl. duplicate unresolved-flag protection |
| Operations summary dashboard endpoint | ✅ Complete | API integration test |
| Docker multi-stage build (non-root) | ✅ Complete | Dockerfile |
What is target architecture (planned, seams exist):
| Component | Status |
|---|---|
| 1D-CNN inference worker (Python, PTB-XL trained) | Planned — Wave 2 |
| SQLite persistence layer | Planned — seam via IEcgCaseRepository |
| PostgreSQL adapter | Planned |
| Lead-specific abnormality detection | Planned |
| SCP-ECG structured report mapping | Planned |
| Feedback loop (clinician corrections → training data) | Planned |
┌──────────────────────────────────────────────────┐
│ Clinician │
│ (Review via API / future UI) │
└──────────────────┬───────────────────────────────┘
│ review / finalize (JWT auth)
┌──────────────────▼───────────────────────────────┐
│ TypeScript API (Express) │
│ │
│ ┌──────────┐ ┌───────────┐ ┌─────────────────┐ │
│ │ Routing │ │ State │ │ Validation │ │
│ │ & Auth │ │ Machine │ │ (Zod) │ │
│ └──────────┘ └───────────┘ └─────────────────┘ │
│ ┌──────────┐ ┌───────────┐ ┌─────────────────┐ │
│ │ Safety │ │ Inference │ │ Health & │ │
│ │ Policy │ │ Service │ │ Metrics │ │
│ └──────────┘ └───────────┘ └─────────────────┘ │
│ ┌──────────┐ ┌───────────┐ ┌─────────────────┐ │
│ │ FHIR R4 │ │ Report │ │ Audit │ │
│ │ Export │ │ Builder │ │ Trail │ │
│ └──────────┘ └───────────┘ └─────────────────┘ │
└──────────────────┬───────────────────────────────┘
│ dispatch / callback
┌──────────────────▼───────────────────────────────┐
│ Inference Worker (planned: Python) │
│ │
│ ┌─────────────────┐ ┌────────────────────────┐ │
│ │ 1D-CNN Classifier│ │ Uncertainty │ │
│ │ (PTB-XL trained) │ │ (MC-Dropout/Ensembles) │ │
│ └─────────────────┘ └────────────────────────┘ │
└───────────────────────────────────────────────────┘
Key design decisions:
- Separation of control and compute: The TypeScript API handles workflow logic only. Heavy neural network inference lives in a separate worker process.
- Async inference with callback: Case creation returns
202 Acceptedimmediately. The inference worker calls back via an internal authenticated endpoint when done (or a failure is captured asInferenceFailed). - Claim discipline: The project distinguishes between what is implemented (backed by running code and tests), what is target architecture (planned), and what is research-informed (supported by literature but not built).
| Layer | Technology |
|---|---|
| Runtime | Node.js ≥ 22, TypeScript 5.8, ES2022 |
| Framework | Express 4 with Helmet, express-rate-limit |
| Validation | Zod 3.24 (strict schemas for all inputs) |
| Auth | API key (operators), HS256 JWT with issuer/audience validation (reviewers), Bearer token (internal) |
| Metrics | prom-client 15 (Prometheus-compatible) |
| Testing | Jest 29, ts-jest, supertest |
| Container | Docker multi-stage build, non-root ecg user |
| Identifiers | Built-in node:crypto.randomUUID() |
| Method | Path | Description |
|---|---|---|
| POST | /api/v1/cases |
Submit ECG recording for second opinion (returns 202) |
| GET | /api/v1/cases |
List all cases |
| GET | /api/v1/cases/:id |
Get case detail with full assessment |
| GET | /api/v1/cases/:id/report |
Structured ECG report |
| GET | /api/v1/cases/:id/exports/fhir-diagnostic-report |
FHIR R4 DiagnosticReport |
| GET | /api/v1/operations/summary |
Operations dashboard summary |
| Method | Path | Description |
|---|---|---|
| POST | /api/v1/cases/:id/review |
Submit clinician review |
| POST | /api/v1/cases/:id/safety-flags/:flagCode/resolve |
Resolve a safety flag |
| POST | /api/v1/cases/:id/finalize |
Finalize case |
| Method | Path | Description |
|---|---|---|
| POST | /api/internal/inference-callback |
Inference worker callback |
| Method | Path | Description |
|---|---|---|
| GET | / |
API identity and route map |
| GET | /healthz |
Liveness probe |
| GET | /readyz |
Readiness probe |
| GET | /metrics |
Prometheus metrics |
- Three-tier authentication: Operator API key for case management, HS256 JWT for clinician review (with role-based access), internal bearer token for inference worker callbacks.
- Timing-safe comparison: All secret comparison uses
crypto.timingSafeEqual. - JWT claim validation: Reviewer tokens validate
alg,iss,aud,exp,nbf,sub, androlebefore access is granted. - Input validation: Every endpoint validates input via Zod schemas with
strict mode — no extra fields accepted, and classifier probabilities must sum
to
1.0 ± 0.001. - Rate limiting: Applied to all API endpoints.
- Helmet: Security headers enabled by default (CSP, HSTS, etc.).
- CORS: Not enabled — add per deployment requirements.
- Non-root container: Docker runs as unprivileged
ecguser. - No PHI in logs: Patient aliases only, never real identifiers.
- Clock skew tolerance: JWT validation includes configurable clock skew window for distributed deployments.
# Install
npm install
# Copy environment config
cp .env.example .env
# Development mode (hot reload)
npm run dev
# Build & run
npm run build
npm start
# Docker
docker compose up --build# 1. Submit case
curl -X POST http://localhost:3100/api/v1/cases \
-H 'Content-Type: application/json' \
-H 'x-api-key: ecg-operator-dev-token-change-me-0001' \
-d '{
"recording": {
"recordingId": "ptbxl-00001",
"patientAlias": "Patient-001",
"recordingDate": "2024-01-15T10:00:00.000Z",
"samplingFrequencyHz": 500,
"leadCount": 12,
"durationSeconds": 10,
"samplesPerLead": 5000,
"sourceDataset": "PTB-XL"
},
"clinicalQuestion": {
"questionText": "Rule out myocardial infarction",
"urgency": "routine"
}
}'
# Response: { "caseId": "...", "status": "InferencePending", "message": "..." }
# 2. Wait for async inference, then review (requires reviewer JWT)
curl -X POST http://localhost:3100/api/v1/cases/{caseId}/review \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <reviewer-jwt>' \
-d '{
"decision": "accepted",
"clinicalNotes": "NSR confirmed. No acute ST changes."
}'
# 3. Finalize
curl -X POST http://localhost:3100/api/v1/cases/{caseId}/finalize \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <reviewer-jwt>' \
-d '{
"outcome": "delivered",
"finalSummary": "Normal sinus rhythm confirmed by cardiologist."
}'Tests: 95 passed, 95 total
Test Suites: 5 passed, 5 total
tests/config.test.ts — Config validation and production safety guards (9 tests)
tests/cases.test.ts — Aggregate state machine and safety-flag invariants (25 tests)
tests/safety-policy.test.ts — Clinical safety rules (13 tests)
tests/validation.test.ts — Zod schema validation and probability invariants (12 tests)
tests/api.test.ts — API integration and auth boundary coverage (36 tests)
npm test # Run all tests
npm run test:watch # Watch mode
npm run lint # ESLint over src/ and tests/
npm run typecheck # TypeScript type-check without emitecg-second-opinion/
├── src/
│ ├── index.ts # Entry point, graceful shutdown
│ ├── app.ts # Express application (11 routes)
│ ├── cases.ts # Root aggregate — 6-state machine
│ ├── case-contracts.ts # All TypeScript interfaces and types
│ ├── case-presentation.ts # Report builders, DTO mappers
│ ├── case-exports.ts # FHIR R4 DiagnosticReport export
│ ├── case-repository.ts # In-memory repository + audit trail
│ ├── validation.ts # Zod schemas for all API inputs
│ ├── safety-policy.ts # 8-rule clinical safety evaluation
│ ├── inference-service.ts # Metadata-fallback stub (@sota-stub)
│ ├── config.ts # Environment-based configuration
│ ├── metrics.ts # Prometheus metrics (7 instruments)
│ ├── health.ts # /healthz and /readyz probes
│ ├── correlation.ts # X-Correlation-Id middleware
│ ├── express-request.d.ts # Express Request augmentation declarations
│ ├── auth-common.ts # Shared auth utilities
│ ├── operator-auth.ts # API key middleware
│ ├── reviewer-auth.ts # JWT HS256 middleware
│ └── internal-auth.ts # Bearer token middleware
├── tests/
│ ├── config.test.ts # Config guards and production-safe defaults
│ ├── cases.test.ts # 25 state machine and flag-invariant tests
│ ├── safety-policy.test.ts # 13 safety policy tests
│ ├── validation.test.ts # 12 schema and probability tests
│ └── api.test.ts # 35 integration tests
├── Dockerfile # Multi-stage, non-root
├── docker-compose.yml
├── .env.example # All environment variables documented
├── tsconfig.json
├── eslint.config.mjs # ESLint flat config
├── jest.config.js
├── package.json
├── LICENSE # MIT
├── CONTRIBUTING.md
├── SECURITY.md
├── CODE_OF_CONDUCT.md
└── README.md
Following the PTB-XL labeling scheme and SCP-ECG standard:
| Code | Category | Description |
|---|---|---|
| NORM | Normal sinus rhythm | No significant abnormalities |
| MI | Myocardial infarction | ST-elevation/depression, Q-wave patterns |
| STTC | ST/T change | Non-specific ST segment or T-wave changes |
| CD | Conduction disturbance | Bundle branch blocks, AV blocks |
| HYP | Hypertrophy | Left/right ventricular hypertrophy |
The DefaultEcgClinicalSafetyPolicy evaluates every inference result against
8 clinical rules aligned with AHA/ACC/HRS guidelines:
| # | Rule | Trigger | Severity | Blocks |
|---|---|---|---|---|
| 1 | LOW_CONFIDENCE | Confidence band low |
Warning | No |
| 2 | INSUFFICIENT_DATA | Confidence band insufficient_data |
Critical | Yes |
| 3 | SIGNIFICANT_DISAGREEMENT | AI disagrees with original interpretation | Warning | No |
| 4 | STAT_MI_DETECTED | MI detected on stat-priority recording | Critical | Yes |
| 5 | SHORT_RECORDING | Duration < 10s | Warning | No |
| 6 | LOW_SAMPLING_RATE | Frequency < 100 Hz | Warning | No |
| 7 | HIGH_EPISTEMIC_UNCERTAINTY | Epistemic entropy > 0.7 (MC-Dropout) | Critical | Yes |
| 8 | NO_XAI_EXPLANATION | No interpretability artifacts available | Info | No |
Blocking flags prevent delivery even after clinician review. They must be
explicitly resolved by a reviewer before the case can be finalized as
delivered.
Based on: Mosin S.G. (2024) "Neural network diagnosis of the cardiovascular diseases based on data-driven method", Software & Systems, 37(1), pp. 122–130. doi: 10.15827/0236-235X.142.122-130.
Key findings from the paper:
- Architecture: 1D multi-layer convolutional neural network for ECG time series classification
- Method: Data-driven approach — no manual PQRST feature extraction needed
- Dataset: PTB-XL — 21,837 12-lead ECG recordings, 10s at 100 Hz (1000 samples/lead)
- Best result: 3-layer CNN with smaller pooling window achieves 85.66% MI detection accuracy
- Activation: ELU (supports negative values inherent in ECG signals)
- Optimizer: Mini-batch Adam with batch normalization
- Classification: Softmax output layer for category probability distribution
The current codebase implements the metadata-fallback mode. Full 1D-CNN inference is planned for Wave 2.
This project lives in the clinical decision support workflow space, but it should not be positioned as relying on the FDA non-device CDS exclusion for a future clinically deployed ECG-signal-analysis product.
Under 21 U.S.C. § 360j(o)(1)(E), the non-device CDS exclusion does not cover software functions intended to acquire, process, or analyze a medical image or a pattern or signal from a signal acquisition system. FDA's January 2026 final guidance on Clinical Decision Support Software clarifies that this boundary matters when software analyzes signal data while supporting diagnosis or treatment decisions.
That is relevant here because the target production architecture includes
1D-CNN analysis of ECG signal data. A clinically deployed version should be
planned as a device-regulated SaMD pathway, not marketed as a non-device
CDS exemption claim. The enforced Reviewed state before Finalized still
matters, but as a human-oversight control, not as a substitute for device
regulatory obligations.
Current status: Research-use-only prototype. Not submitted for FDA review. Not CE-marked. Not approved for clinical use.
Strategic regulatory workstreams for future clinical deployment:
- U.S. pathway analysis:
510(k)vsDe Novodepending on intended claims, predicates, and risk framing - AI/ML lifecycle planning, including PCCP-style change governance for future model updates
- GMLP-aligned lifecycle evidence, including drift monitoring and post-market performance review
- EU AI Act preparation for medical-deployment documentation, human oversight, and data-governance obligations
- Sprint 1 — Infrastructure hardening: replace
setImmediate()dispatch with a durable queue, bounded retry policy, dead-letter or replay path, and durable PostgreSQL-backed persistence - Sprint 2 — Regulatory and lifecycle compliance: document SaMD-oriented positioning, add GMLP work products, define PCCP-style update governance, and prepare EU AI Act evidence surfaces for medical deployment
- Sprint 3 — Clinical interoperability: add SNOMED CT / LOINC coding, strengthen FHIR profile declarations, and plan HL7 aECG / DICOM Waveform support for enterprise environments
- Sprint 4 — Real inference stack: implement the Python 1D-CNN worker, MC-Dropout uncertainty, Grad-CAM-style explainability, and drift-aware monitoring around PTB-XL-trained models
- Issues: GitHub Issues
- Contributing: See CONTRIBUTING.md
- Security: See SECURITY.md
- Code of Conduct: See CODE_OF_CONDUCT.md
- Support: See SUPPORT.md
- Citation: See CITATION.cff
MIT — see LICENSE.
Система второго мнения по ЭКГ с обязательным контролем врача-кардиолога.
Автономный TypeScript API, который управляет полным жизненным циклом ЭКГ-консультации — от приёма записи и проверки качества через AI-анализ до обязательного врачебного заключения, финализации и выдачи.
⚠️ Только для исследовательских целей. Репозиторий не имеет FDA-clearance, не имеет CE-marking и не предназначен для клинического применения. Система не может использоваться для принятия клинических решений. Каждый результат требует проверки квалифицированным врачом.
ECG Second Opinion — это не «ИИ, который читает ЭКГ». Это система управления рабочим процессом вокруг ИИ — контрольная плоскость, которая гарантирует, что каждый случай ЭКГ следует строгому, проверяемому пути от подачи до выдачи.
- Клиницист или интеграционная система подаёт 12-канальную ЭКГ-запись с псевдонимом пациента и метаданными записи
- Система ставит запись в очередь на ИИ-анализ (1D-CNN классификатор)
- Инференс-воркер обрабатывает запись — классификация, оценка уверенности, метрики неопределённости
- Система фиксирует результат ИИ как черновой структурированный отчёт — не как финальный диагноз
- Политика клинической безопасности оценивает результат и выставляет флаги для случаев с низкой уверенностью, высоким риском или расхождением
- Врач-кардиолог обязан проверить черновик, добавить своё заключение и явно подтвердить или изменить его
- Только после врачебного одобрения случай переходит к финализации и выдаче
- Каждый шаг логируется, помечается временем и отслеживается через неизменяемый журнал аудита
Ключевая идея: ИИ генерирует черновик. Решение принимает человек. Система обеспечивает эту границу в коде.
Submitted → InferencePending → AwaitingReview → Reviewed → Finalized
↓
InferenceFailed
Каждый переход защищён инвариантами. Агрегат отклоняет недопустимые изменения состояния с типизированными доменными ошибками.
| Компонент | Статус | Доказательство |
|---|---|---|
| Агрегат с 6 состояниями | ✅ Реализован | 25 юнит-тестов |
| 3-уровневая аутентификация | ✅ Реализована | Интеграционные тесты |
| Zod-валидация (все эндпоинты) | ✅ Реализована | 12 тестов валидации |
| Политика безопасности (8 правил) | ✅ Реализована | 13 тестов |
| Метаданные-фолбэк инференс | ✅ Стаб | @sota-stub |
| FHIR R4 DiagnosticReport экспорт | ✅ Реализован | Интеграционные тесты |
| Prometheus-метрики (7 инструментов) | ✅ Реализованы | Подключены в роутах |
| Неизменяемый журнал аудита | ✅ Реализован | Тест аудита |
| Слой | Технология |
|---|---|
| Среда выполнения | Node.js ≥ 22, TypeScript 5.8, ES2022 |
| Фреймворк | Express 4 + Helmet + express-rate-limit |
| Валидация | Zod 3.24 |
| Аутентификация | API-ключ, HS256 JWT с проверкой issuer/audience, Bearer-токен |
| Метрики | prom-client 15 |
| Тестирование | Jest 29, ts-jest, supertest |
| Контейнер | Docker multi-stage, непривилегированный пользователь |
| Метод | Путь | Описание |
|---|---|---|
| POST | /api/v1/cases |
Подать ЭКГ-запись для второго мнения |
| GET | /api/v1/cases |
Список случаев |
| GET | /api/v1/cases/:id |
Детали случая |
| GET | /api/v1/cases/:id/report |
Структурированный отчёт |
| GET | /api/v1/cases/:id/exports/fhir-diagnostic-report |
FHIR R4 экспорт |
| Метод | Путь | Описание |
|---|---|---|
| POST | /api/v1/cases/:id/review |
Врачебное заключение |
| POST | /api/v1/cases/:id/finalize |
Финализация случая |
npm install
cp .env.example .env
npm run devТесты: 95 пройдено, 95 всего (5 наборов)
npm testМосин С.Г. (2024) «Нейросетевая диагностика заболеваний сердечно-сосудистой системы на основе метода, управляемого данными», Программные продукты и системы, 37(1), с. 122–130. doi: 10.15827/0236-235X.142.122-130.
Проект находится в области workflow-систем для клинической поддержки решений, но его нельзя описывать как систему, которая в продакшене будет полагаться на FDA-исключение для non-device CDS.
Согласно 21 U.S.C. § 360j(o)(1)(E) и финальному руководству FDA по Clinical Decision Support Software от января 2026 года, исключение не распространяется на ПО, которое предназначено для получения, обработки или анализа медицинского изображения либо сигнала из системы съёма физиологических данных.
Это важно для данного проекта, потому что целевая продакшен-архитектура
включает 1D-CNN анализ ЭКГ-сигналов. Поэтому клинически развёрнутая версия
должна планироваться как device-regulated SaMD, а не как non-device CDS.
Жёсткая обязательность стадии Reviewed перед Finalized остаётся важной, но
уже как механизм human oversight, а не как замена регуляторному пути.
Текущий статус: Исследовательский прототип. Не подавался на FDA review. Не имеет CE-marking. Не допущен к клиническому применению.
- Sprint 1 — Инфраструктурное усиление: заменить
setImmediate()на durable queue, добавить bounded retry / dead-letter path и перейти к долговечной PostgreSQL-персистентности - Sprint 2 — Комплаенс и жизненный цикл модели: зафиксировать SaMD-путь, подготовить GMLP-артефакты, PCCP-подобное управление обновлениями и evidence-поверхность под EU AI Act для медицинского применения
- Sprint 3 — Медицинская интероперабельность: добавить SNOMED CT / LOINC, усилить FHIR profile declarations и спланировать HL7 aECG / DICOM Waveform для enterprise-интеграций
- Sprint 4 — Реальный inference stack: Python 1D-CNN worker, MC-Dropout, Grad-CAM/XAI и drift-aware monitoring для моделей на PTB-XL
MIT — см. LICENSE.