Kardo

Dokumentacja techniczna

Jak działa Kardo, symulator kardiomonitora klasy szpitalnej: architektura, model fizjologii, synteza sygnałów i warstwa bezpieczeństwa dydaktycznego.

wersja 0.3.1autor: Piotr Choińskicommit 8ee2402196 testów silnika18 rozdziałów
Spis treści · 18 rozdziałów
  1. 1.Wprowadzenie
  2. 2.Architektura systemu
  3. 3.Model fizjologii: skąd biorą się wartości liczbowe
  4. 4.Synteza przebiegów: jak powstają wykresy
  5. 5.Rytmy serca
  6. 6.Oś tętna i inwariant braku perfuzji
  7. 7.Model zatrzymania krążenia
  8. 8.Farmakodynamika i bezpieczeństwo lekowe
  9. 9.Interwencje kliniczne
  10. 10.Alarmy
  11. 11.Zdarzenia kliniczne i choroby
  12. 12.Tryby pracy i macierz uprawnień
  13. 13.Warstwa AI/RAG
  14. 14.Persystencja i baza danych
  15. 15.Jakość, testy i walidacja
  16. 16.Źródła wiedzy klinicznej
  17. 17.Ograniczenia, dług techniczny i dalszy rozwój
  18. 18.Referencje

1Wprowadzenie

Na ekranie monitora migoce migotanie komór, a tuż pod krzywą pulsoksymetr pokazuje SpO₂ 98%. Taki obraz jest fizjologicznie niemożliwy: bez skutecznego wyrzutu serca nie ma fali tętna, którą pulsoksymetr mógłby zmierzyć, a mimo to pojawia się w symulatorach opartych na odtwarzaniu zapętlonych nagrań, w których krzywa i liczba pochodzą z dwóch niezależnych źródeł. Kardo powstało wokół jednej decyzji, która ma temu zapobiec: cała dynamika sygnałów jest liczona z jednego, wspólnego stanu pacjenta, a nie odtwarzana.

To webowy symulator kardiomonitora klasy szpitalnej, wzorowany na monitorach Philips IntelliVue i GE Carescape. Renderuje w czasie rzeczywistym osiemnaście kanałów sygnału: dwanaście odprowadzeń EKG oraz pletyzmogram, inwazyjne ciśnienie tętnicze, ośrodkowe ciśnienie żylne, ciśnienie w tętnicy płucnej, oddech impedancyjny i kapnografię. Obsługuje 26 rytmów serca o odróżnialnej morfologii, farmakodynamikę leków, defibrylator zgodny z wytycznymi ERC, cztery tryby pracy dydaktycznej oraz opcjonalną, lokalną warstwę AI, z zerową tolerancją na błąd w przebiegach.

Kolejność rozdziałów wynika z jednej potrzeby: oddzielenia modelu fizjologii od jego prezentacji. Najpierw architektura i przepływ danych (rozdz. 2), potem osobno źródło wartości liczbowych (rozdz. 3) i osobno synteza przebiegów (rozdz. 4), następnie warstwy kliniczne (rozdz. 5–11), organizacja pracy i uprawnień (rozdz. 12), warstwa AI i dane (rozdz. 13–16), a na końcu uczciwy rachunek ograniczeń (rozdz. 17).

Zasada przewodnia całego systemu: każda wyświetlana liczba jest pochodną tego samego stanu fizjologicznego co przebiegi, nie niezależną etykietą. Z tej zasady wynika większość opisanych niżej decyzji projektowych.

2Architektura systemu

2.1. Monorepo i podział odpowiedzialności

Kod jest zorganizowany jako monorepo (npm workspaces) z wyraźnym rozdziałem silnika od interfejsu i backendu.

Pakiet / aplikacjaOdpowiedzialnośćStos
packages/coreSilnik: synteza sygnałów, scheduler zdarzeń, alarmy, farmakodynamika, detektory, trendy. TypeScript bez zależności od DOM.TypeScript
packages/modelsTypy domenowe (PatientState, SimEvent, Alarm, Rhythm) i macierz uprawnień (can(), MATRIX).TypeScript
packages/audioTony alarmów i defibrylatora wg IEC 60601-1-8.Web Audio API
packages/scenariosBiblioteka gotowych przypadków klinicznych.TypeScript
apps/webAplikacja kliencka: React SPA, renderowanie Canvas 2D, panele kliniczne, tryby.React 18, Vite, Tailwind
apps/apiBackend: REST, WebSocket, baza, warstwa AI.Fastify, PostgreSQL, Drizzle ORM, pgvector

Rozdział core/models od apps/web jest celowy: silnik nie zna DOM-u ani Reacta, więc daje się testować w izolacji i uruchamiać w Web Workerze. To pozwala trzymać całą fizjologię z dala od wątku interfejsu.

2.2. Dwa wątki, jeden strumień próbek

Cała symulacja działa w Web Workerze, osobnym od wątku UI (sim-worker.ts). Worker liczy próbki wszystkich kanałów ze stałą częstotliwością i wysyła je do interfejsu partiami; UI tylko rysuje. Timing opiera się na performance.now() i pętli setTimeout o kroku ~4 ms.

  • SAMPLE_RATE_HZ = 250: częstotliwość próbkowania sygnałów; SAMPLE_INTERVAL_MS = 4.
  • BATCH_INTERVAL_MS = 10: co 10 ms (100 Hz) worker wysyła partię próbek (Float32Array per kanał). Przy 60 Hz odświeżania daje to 2–3 próbki na klatkę, strumień gęstszy niż tempo renderowania, bez luk.
  • SNAPSHOT_INTERVAL_MS = 1000: co sekundę (1 Hz) worker wysyła pełny PatientState z wartościami liczbowymi, zgodnie z konwencją Philips IntelliVue / GE Carescape.
  • Trendy co 5000 ms; osobny sygnał heart_beat przy każdym skurczu komorowym (do tonu QRS i migotania diody).

Interfejs (SimProvider.tsx, WaveformChannel.tsx) odbiera partie do buforów pierścieniowych i rysuje przebieg na Canvas 2D w pętli requestAnimationFrame, w trybie przesuwu, jak realny monitor. Dzięki wyniesieniu obliczeń do workera interfejs utrzymuje 60 klatek na sekundę nawet przy kilkunastu sprzężonych przebiegach.

Uwaga architektoniczna. Wczesne notatki v1 opisywały renderowanie przez OffscreenCanvas w workerze. W kodzie tego nie ma: worker wyłącznie liczy próbki, a Canvas 2D rysowany jest na wątku głównym (sprostowane 2026-07-16).

2.3. Kanały sygnału

Worker generuje zawsze pełny komplet 18 kanałów: dwanaście odprowadzeń EKG (I, II, III, aVR, aVL, aVF, V1–V6) oraz sześć kanałów niekardiograficznych: PLETH, ABP, CVP, PAP, RESP, CO2.

2.4. Role: Controller, Display, Instructor

Role są ortogonalne do trybu pracy (rozdz. 12) i decydują o kierunku przepływu sterowania.

  • Controller prowadzi symulację: jego worker liczy fizjologię, a każda komenda trafia równolegle do własnego workera i na transport synchronizujący (fan-out do okien Display).
  • Display to monitor przy łóżku, pasywny, tylko do odczytu. Ma własną instancję workera, ale pomija ona scheduler, silnik i NIBP; generuje jedynie przebiegi z otrzymanego stanu. Dzięki temu wiele ekranów pokazuje identyczny obraz bez ponownego liczenia.
  • Instructor (tryb egzaminacyjny) steruje sesją zdalnie: wstrzykuje zdarzenia, zmienia parametry i ocenia, ale nie wykonuje interwencji klinicznych.

2.5. Synchronizacja wielu okien

Synchronizacja działa przez wspólny interfejs SyncTransport z dwoma backendami (sync-transport.ts):

  • BroadcastChannel o nazwie cardiomon-sync: domyślny, bezkonfiguracyjny, dla okien w obrębie jednej maszyny/pochodzenia.
  • WebSocket: gdy ustawiony VITE_WS_URL; łączy przez serwer (endpoint GET /ws/sync?role=&room=), co pozwala rozdzielić Controller i Display na różne maszyny w LAN. Serwer prowadzi pokoje, robi fan-out komend według ról i cache'uje ostatni stan Controllera (warm-join).

Controller rozgłasza pełny stan (__sync_state__) co 2,5 sekundy (HEARTBEAT_MS = 2500), a świeżo otwarty Display prosi o stan komendą __sync_request__.

Istotny mechanizm bezpieczeństwa: komendy instruktora relayowane przez serwer są autorytatywnie stemplowane znacznikiem _origin: 'instructor', dzięki czemu worker studenta waliduje je jako uprawnienia instruktora, a nie studenta (rozdz. 12).

3Model fizjologii: skąd biorą się wartości liczbowe

Wyświetlane parametry nie są przechowywane obok przebiegów. Są z nich wyprowadzane co takt symulacji przez silnik fizjologii (physiology-engine.ts). To realizacja zasady z rozdziału 1. Poniższe wzory pokazują, jak z demografii pacjenta i bieżącej hemodynamiki powstają liczby na monitorze.

3.1. Rzut serca i wskaźnik sercowy

Objętość wyrzutowa liczona jest z masy ciała, ze współczynnikiem zależnym od grupy wiekowej i korektą na skrócone napełnianie przy tachykardii:

SV [mL] = masa[kg] × svPerKg × filling svPerKg: dorosły 0.9 · dziecko 1.2 · niemowlę 1.2 · noworodek 1.3 filling = 1.0, a gdy HR > próg tachykardii (dorosły 120): maleje do min. 0.45

Rzut serca sprzęgnięty jest z ciśnieniem, bo w hipotensji i wstrząsie wyrzut spada, a nie żyje własnym życiem z samego tętna:

CO [L/min] = HR × SV / 1000 × perfFactor perfFactor = clamp(MAP / 90, 0.15, 1.1) CI [L/min/m²] = CO / BSA

Powierzchnia ciała liczona jest wzorem DuBois:

BSA [m²] = 0.007184 × masa[kg]^0.425 × wzrost[cm]^0.725

Ten szczegół ma znaczenie kliniczne. Stałe dorosłego (SV 70 mL, BSA 1,7 m²) dawały noworodkowi rzut serca ~9,8 L/min zamiast ~0,7, błąd rzędu wielkości. Wyprowadzenie z demografii usuwa go u źródła.

3.2. Ciśnienie i wskaźnik perfuzji

Ciśnienie średnie liczone jest klasycznie:

MAP = rozkurczowe + (skurczowe − rozkurczowe) / 3

Wskaźnik perfuzji (PI) jest funkcją utlenowania, ciśnienia i tętna, z bazową wartością 2,5 przy SpO₂ 98% i MAP 90 mmHg, dochodzącą do celu ze stałą czasową ~8 s:

PI_cel = 2.5 × f(SpO₂) × f(MAP) × kara(HR) f(SpO₂): 60% → 0.0, 98% → ~1.06 f(MAP): 40 → 0.0, 65 → 0.5, 90 → 1.0 kara(HR): dla HR > 140 spada do min. 0.4

3.3. Sprzężenia międzyparametrowe

Silnik utrzymuje kilka pętli sprzężenia zwrotnego, które sprawiają, że zmiana jednego parametru pociąga inne:

  • Skrajna tachykardia (HR > 150) obniża MAP przez skrócone napełnianie rozkurczowe.
  • Niskie MAP (< 60) wyzwala kompensacyjną tachykardię z baroreceptorów: +0,8 uderzenia/min na każdy mmHg poniżej 60, do +30.
  • Kapnografia sprzężona z wentylacją: PaCO₂ ≈ EtCO₂ + 3–5 mmHg (gradient a–A).
  • Jakość pletyzmogramu (plethQuality) jest zawsze przeliczana z bieżącego PI, co bramkuje alarm słabej perfuzji.

4Synteza przebiegów: jak powstają wykresy

Każdy przebieg jest liczony z modelu matematycznego, próbka po próbce, a nie odtwarzany. Wspólną podstawą jest kilka funkcji pomocniczych (utils/math.ts): funkcja Gaussa gaussian(x,μ,σ) = exp(−(x−μ)²/2σ²), deterministyczny generator liczb pseudolosowych mulberry32 (do odtwarzalnych artefaktów i jitterów) oraz szum gaussowski metodą Boxa–Mullera. Determinizm PRNG jest ważny: te same warunki dają ten sam przebieg, co jest warunkiem testowalności.

4.1. EKG: suma Gaussianów

Sygnał EKG powstaje jako suma pięciu funkcji Gaussa odwzorowujących załamki P, Q, R, S i T w każdym cyklu (synth/ecg.ts):

ecg(t) = Σ aᵢ · exp( −(t − μᵢ)² / 2σᵢ² ) dla i ∈ {P, Q, R, S, T}

Parametry bazowe skalibrowano dla odprowadzenia II i rytmu zatokowego przy HR 60 (BASE_PQRST), z jawnym uzasadnieniem klinicznym każdej wartości w komentarzach kodu:

ZałamekAmplitudaPozycja μ (ms od R)Szerokość σ (ms)
P0,20 mV−17532
Q−0,10 mV−256
R1,30 mV08
S−0,25 mV227
T0,55 mV19038

Z tych parametrów wynikają odstępy w granicach normy: QRS ≈ 80–90 ms, QT ≈ 330 ms, PR ≈ 150–200 ms.

Morfologia dwunastu odprowadzeń różnicowana jest macierzą wzmocnień per odprowadzenie (LEAD_GAIN), osobno dla P, QRS i T. Odprowadzenie aVR ma współczynniki ujemne (QRS −1,0) , stąd jego odwrócony zespół. Na to nakładają się: przesunięcie odcinka ST per odprowadzenie (stShiftMv, podstawa detekcji STEMI), warianty załamka T (prawidłowy, nadostry, odwrócony, dwufazowy, płaski, spiczasty) oraz sprzężenie zatokowej arytmii oddechowej z rzeczywistym torem oddechu.

4.2. Pletyzmogram

Pletyzmogram (synth/pleth.ts) ma kształt zbliżony do fali tętniczej, ale z opóźnieniem ~200 ms względem EKG (PLETH_DELAY_MS = 200), modulacją oddechową (PVI) i amplitudą proporcjonalną do wskaźnika perfuzji. Przy braku perfuzji przebieg spłaszcza się do samego szumu; przy niskim PI (< 0,5) amplituda wyraźnie maleje.

4.3. Inwazyjne ciśnienie tętnicze

Krzywa ABP (synth/abp.ts) odwzorowuje falę w tętnicy promieniowej: czas przejścia ~150 ms od załamka R, strome ramię skurczowe (20–40 ms), wcięcie dykrotyczne przy zamknięciu zastawki aortalnej (~250 ms po szczycie) i powolny spadek rozkurczowy ze stałą czasową ~300 ms. Cały cykl trwa jeden odstęp R–R. W migotaniu przedsionków amplitudy są zmienne uderzenie do uderzenia (krótszy R–R = mniejsze napełnianie = niższe skurczowe). Modelowane są też artefakty linii tętniczej: szybkie płukanie i tłumienie.

4.4. Pozostałe przebiegi hemodynamiczne i oddechowe

  • Ośrodkowe ciśnienie żylne (cvp.ts): fale a/c/v z zejściami x i y, amplituda 1–3 mmHg, modulacja oddechowa; w migotaniu przedsionków zanika fala a, w niedomykalności trójdzielnej rośnie fala v.
  • Ciśnienie w tętnicy płucnej (pap.ts): czas przejścia ~100 ms, wartości sys 15–30 / dia 4–12 mmHg, mniej wyraźne wcięcie dykrotyczne niż w ABP.
  • Oddech impedancyjny (resp.ts): sinus o częstości RR/60 Hz z drobnymi oscylacjami kardiogennymi; w bezdechu przebieg płaski.
  • Kapnografia (capno.ts): czterofazowy kształt (linia podstawowa, ramię wznoszące, plateau, faza wdechowa) z dziesięcioma wariantami: prawidłowy, „płetwa rekina" (obturacja), „szczelina kurary" (resztkowa miorelaksacja), rosnąca linia podstawowa, oscylacje sercowe, wydłużone plateau, hipo- i hiperwentylacja, intubacja przełyku (malejące amplitudy do zera) oraz płaski (bezdech/rozłączenie).

5Rytmy serca

System modeluje 26 rytmów, z których każdy ma odróżnialną morfologię, nie jest skalowanym rytmem zatokowym. Pełna lista pochodzi z typu Rhythm (packages/models/src/index.ts):

GrupaRytmy
ZatokoweSR (zatokowy), SB (bradykardia), ST (tachykardia), SA (arytmia oddechowa)
NadkomoroweAF (migotanie przedsionków), AFL (trzepotanie), AT (częstoskurcz przedsionkowy), AVNRT (nawrotny węzłowy), WPW (preekscytacja, fala delta), JR (rytm węzłowy)
KomoroweVT (jednokształtny), VT_POLY (wielokształtny), TDP (torsades de pointes), VF (migotanie komór), VFL (trzepotanie komór)
Zastępcze / zatrzymanieIVR (idiowentrykularny), AIVR (przyspieszony), ASY (asystolia), PEA (czynność elektryczna bez tętna)
Bloki AVAVB_I (I°), AVB_II_M1 (Mobitz I / Wenckebach), AVB_II_M2 (Mobitz II), AVB_III (całkowity)
StymulacjaPACED_A (przedsionkowa), PACED_V (komorowa), PACED_AV (sekwencyjna)

Odróżnialność morfologiczna jest wymogiem, nie ozdobą. Trzepotanie przedsionków ma falę piłokształtną z blokiem przewodzenia; migotanie komór zanika amplitudą od grubo- do drobnofalistego; torsades skręca oś wokół linii izoelektrycznej; blok Wenckebacha pokazuje stałe odstępy P–P przy skracającym się R–R aż do pauzy. Poprawność tych morfologii jest utrwalona testami regresyjnymi (rozdz. 15).

6Oś tętna i inwariant braku perfuzji

Najważniejsza pojedyncza decyzja projektowa dotyczy rozdzielenia rytmu elektrycznego od tętna. W realnym pacjencie rytm na EKG nie gwarantuje skutecznego wyrzutu , stąd rozkojarzenie elektromechaniczne w PEA czy brak perfuzji w migotaniu komór. Symulator odwzorowuje to jako twardy inwariant, z jednym źródłem prawdy (utils/perfusion.ts).

ALWAYS_PULSELESS_RHYTHMS = { VF, VFL, ASY, PEA } — tętno niemożliwe z definicji USUALLY_PULSELESS_RHYTHMS = { TDP, VT_POLY } — domyślnie bez tętna, nadpisywalne flagą VT (jednokształtny) — domyślnie z tętnem; pVT ustawiane jawnie

Funkcja isPulseless(rhythm, pulseless) jest pytana bezpośrednio przez wszystkie elementy zależne od perfuzji: syntezatory pletyzmogramu, ABP, PAP i CVP (płaski przebieg), warstwę numeryczną (SpO₂ i ciśnienie nieważne, --), model zatrzymania krążenia i logikę defibrylatora. Reguła obowiązuje w obie strony i nie ma wyjątku „płaski przebieg z żywą liczbą".

Dlaczego jedno źródło prawdy? Bo rozjazd „przebieg płaski, ale liczba idzie normalnie" brał się dokładnie z rozproszenia tej decyzji: przebiegi bramkowały się nazwą rytmu, a numeryka zależała od PI/MAP zerowanych tylko na jednej ze ścieżek zmiany rytmu (inne, jak setParameter, ją omijały). Trzymając zbiór w jednym miejscu i pytając o rytm wprost, gwarantuję spójność niezależnie od tego, jak rytm został ustawiony.

7Model zatrzymania krążenia

Zatrzymanie krążenia nie jest pojedynczym stanem, lecz procesem z oknem odwracalności (data/arrest-model.ts, ARREST_MODEL). Model rozróżnia okna „no-flow" zależne od mechanizmu:

  • rytm defibrylowalny: ~20 min,
  • asystolia: ~10 min,
  • PEA: ~15 min,

z progami nieodwracalności (terminalMs) oraz degeneracją migotania komór w asystolię (vfToAsystoleMs ~12 min). Przepływ podczas RKO ma trzy poziomy jakości (dobry 0,3 / normalny 0,5 / słaby 0,8), a skuteczność defibrylacji maleje ze stałą czasową (defibDecayTauMin ~8 min bez skutecznej resuscytacji). Hipotermia wydłuża okno odwracalności, zgodnie z zasadą „nie jest martwy, dopóki nie jest ciepły i martwy".

Podczas braku perfuzji EtCO₂ spada o ~1 mmHg/s do zera (brak transportu CO₂ do płuc). Źródłem klinicznym modelu jest notatka 05-Scenariusze/Downtime-i-nieodwracalnosc.md oparta na wytycznych ERC 2021 / AHA.

8Farmakodynamika i bezpieczeństwo lekowe

8.1. Model Hilla

Leki działają deterministycznie, bez udziału modelu językowego (pharma/drug-pd.ts). Odpowiedź na dawkę liczona jest z krzywej Hilla:

effect(dawka) = emax · dawka^h / (ec50^h + dawka^h) dawka = ec50 → połowa emax dawka ≫ ec50 → efekt dąży do emax (saturacja)

Parametry emax, ec50 i współczynnik Hilla h pochodzą z danych leku (drugs.pd), a maksymalna zmiana parametru jest ograniczana (DELTA_CAP per cel). Determinizm oznacza, że efekt leku jest w pełni odtwarzalny i niezależny od dostępności AI.

8.2. Trzynaście reguł kontraindykacji

Niezależnie od tego, co zwróci warstwa AI, trzynaście deterministycznych reguł egzekwuje znane niebezpieczne interakcje (contraindications.ts). Każda reguła dopisuje zdarzenia lub wymusza zmianę rytmu, z oceną istotności (krytyczna / ciężka / umiarkowana).

#WarunekSkutek
1β-bloker + astma/POChP (J45/J44)bronchospazm (RR↑, SpO₂↓, kapnogram „płetwa rekina")
2Lidokaina + blok AV III° lub Mobitz IIasystolia
3Werapamil/diltiazem + β-bloker (przewlekle)ciężka bradykardia → blok AV
4Adenozyna + WPW z AF/AFLmigotanie komór (przyspieszenie drogą dodatkową)
5Opioid + POChPdepresja oddechowa (retencja CO₂)
6Opioid + sedatyw (BZD)synergia depresji OUN
7KCl i.v. bolus > 20 mEqasystolia (hiperkaliemia)
8β-bloker + blok AV II M2 / III°pogłębienie bloku → asystolia
9Flekainid/propafenon + strukturalna choroba sercaproarytmia komorowa VT (badanie CAST)
10Werapamil i.v. + niewydolność sercawstrząs kardiogenny
11Adenozyna + astma/POChPbronchospazm
12Sukcynylocholina + hiperkaliemia (K⁺ ≥ 5,5)VT/VF
13NLPZ/aspiryna + niewydolność nereknefrotoksyczność + ryzyko krwawienia (ostrzeżenie)

Reguły są czystymi funkcjami, testowalnymi, bez efektów ubocznych, operującymi na kodach ICD-10 chorób pacjenta, jego lekach przewlekłych i bieżącym rytmie.

9Interwencje kliniczne

9.1. Defibrylator i kardiowersja

Logika defibrylatora opiera się na wytycznych ERC 2021 / AHA 2020 dla defibrylacji dwufazowej (engine/defibrillator.ts). Decyzja o synchronizacji zależy od osi tętna, nie od samej częstości:

  • VF / pVT / VFL / TDP → wstrząs asynchroniczny (unsync); zalecane pierwsze wyładowanie ≥ 150 J,
  • jednokształtny VT z tętnem → kardiowersja synchroniczna,
  • SVT / AF z szybką odpowiedzią → kardiowersja synchroniczna 50–200 J,
  • zakres energii dla dorosłego 120–360 J.

Model zawiera bezpiecznik: włączenie synchronizacji na rytmie bez wyczuwalnego tętna (np. sync na VF) przerywa wyładowanie ze statusem „aborted" i instrukcją wyłączenia sync. Skuteczność narasta z energią: prawdopodobieństwo konwersji rośnie od ~30% przy 100 J do ~85% przy 250 J, przemnożone przez czynnik zaniku (downtime). Modelowane są też realne komplikacje: ~15% szans na ogłuszenie mięśnia i przejściową asystolię po skutecznym wyładowaniu, a przy powrocie krążenia tętno post-ROSC w zakresie 90–120/min.

9.2. Pozostałe interwencje

System obsługuje pomiar nieinwazyjnego ciśnienia (NIBP z konfigurowalnym interwałem), linię tętniczą z szybkim płukaniem, resuscytację krążeniowo-oddechową, intubację, monitorowanie zwiotczenia (TOF), głębokości znieczulenia (BIS), ciśnienia śródczaszkowego i perfuzji mózgowej (ICP/CPP) oraz cewnik Swana-Ganza. To zestaw parametrów właściwy dla stanowiska anestezjologicznego i intensywnej terapii, nie tylko kardiologicznego.

10Alarmy

Silnik alarmów realizuje normę IEC 60601-1-8 (engine/alarm-engine.ts). Każdy alarm ma priorytet (HIGH / MEDIUM / LOW / INFO), warunek wyzwalający i opóźnienie odfiltrowujące drgania (debounce), a tony audio generuje packages/audio zgodnie z melodycznymi wzorcami normy.

Przykładowa hierarchia priorytetów: skrajna tachy-/bradykardia, arytmie złośliwe, skrajnie niskie SpO₂ i skrajnie niskie MAP to alarmy HIGH; wartości poza progami roboczymi (RR, EtCO₂, umiarkowana hipotensja) to alarmy MEDIUM.

Dwa mechanizmy bramkowania zapobiegają fałszywym alarmom z artefaktów:

  • słaby pletyzmogram (poor-pleth) bramkuje alarm SpO₂,
  • stłumiona linia tętnicza (damped ART) bramkuje alarm MAP.

Osobną rodziną są wzorce STEMI wykrywane z mapy przesunięć ST wg czwartej uniwersalnej definicji zawału: uniesienie ≥ 0,1 mV (1 mm) w co najmniej dwóch sąsiednich odprowadzeniach jednego terytorium. Rozróżniane są ściana dolna (II, III, aVF), przednia (V1–V4), boczna (I, aVL, V5, V6) oraz tylna (depresja ST V1–V3 jako ekwiwalent STEMI wymagający pilnej koronarografii).

11Zdarzenia kliniczne i choroby

System rozróżnia dwa mechanizmy zdarzeń, które łatwo pomylić.

11.1. Profile stage-based (katalog ostrych zdarzeń)

Trzynaście profili odpalanych ręcznie z katalogu (data/clinical-events.ts), w trzech kategoriach:

  • Krwawienie (9): śledzionowe, wątrobowe, z górnego i dolnego odcinka przewodu pokarmowego, tętniak aorty, ciąża pozamaciczna, krwiak opłucnej, miedniczno-kończynowe, poporodowe.
  • Udar (3): niedokrwienny (okluzja MCA), krwotoczny śródmózgowy (z ICP), podpajęczynówkowy.
  • Wstrząs (1): progresja klas krwotoku wg ATLS (I → IV → PEA).

Każdy profil to tabela etapów (stages) nieleczonej progresji: dla danej sekundy atSec zestaw docelowych wartości parametrów osiąganych rampą. Kluczowe jest to, że interpretuje je jeden, generyczny mechanizm (applyClinicalProfile): dodanie zdarzenia to wpis w tablicy danych, bez zmian w kodzie silnika i bez rozgałęzień per rozpoznanie.

11.2. Stochastyczne zdarzenia chorobowe

Drugi mechanizm to losowe zdarzenia wynikające z profilu chorobowego pacjenta, opisanego w bazie kolumną physiologyImpact (JSONB na tabeli diseases). Ładunek zawiera offsety parametrów jako zakresy [min, max], docelowy rytm, przesunięcia ST oraz listę zdarzeń chorobowych z prawdopodobieństwem na minutę. Harmonogram (disease-event-scheduler.ts) co ~5 s przelicza prawdopodobieństwo z modyfikatorami: leki ochronne (β-bloker w STEMI zmniejsza ryzyko VT), wielochorobowość (niewydolność serca + arytmia zwiększa ryzyko) i bieżące parametry (hipotensja sprzyja desaturacji).

Prawdopodobieństwo na minutę przeliczane jest na takt wzorem pTick = 1 − (1−pMin)^dtMin, a generator jest deterministyczny i seedowany, więc cała dynamika stochastyczna działa bez AI.

11.3. Bramka plauzybilności

W trybach scenario, story i exam zdarzenia patologiczne przechodzą przez bramkę plauzybilności (engine/event-plausibility.ts), której punktem kontrolnym jest obsługa add_event w workerze. Bramce podlegają tylko zdarzenia wnoszące patologię (zmiana rytmu, zmiana ST, odma, anafilaksja, krwawienie, zaciśnięcie chirurgiczne, wymuszona ekstubacja, nacięcie); leki, akcje kliniczne, powrót do rytmu zatokowego i normalizacja ST przechodzą zawsze.

Reguła: zdarzenie patologiczne jest dozwolone, gdy ma pokrycie w profilu chorobowym pacjenta (isEventGrounded) lub niesie werdykt walidatora RAG różny od block. W przeciwnym razie zostaje odrzucone, i to nie po cichu: worker emituje event_rejected, a interfejs pokazuje powiadomienie. Tylko tryb sandbox jest w pełni wolny; story również bramkuje per pacjent.

12Tryby pracy i macierz uprawnień

12.1. Cztery tryby

TrybRola dydaktycznaZasada
Sandboxtest / instruktorWszystko wolno: dowolne wstrzyknięcia bez podłoża fizjologicznego.
Scenariogra soloPełna karta pacjenta, tylko biologicznie możliwe zdarzenia, panel leków, postęp zapisywany w bazie.
Storyprowadzenie pacjentaDynamika wyłącznie ze zdarzeń chorobowych, czas rzeczywisty.
Examegzamin z nadzoremStudent z zablokowanym interfejsem i timerem; instruktor planuje zdarzenia i ocenia zdalnie.

12.2. Trójwarstwowa egzekucja uprawnień

Uprawnienia wynikają z macierzy Rola × Tryb → zbiór zdolności (capabilities.ts, funkcja can()), z domyślną odmową: co nie jest w zbiorze, jest zabronione. Macierz jest egzekwowana na trzech warstwach, żeby żadna pojedyncza nie była jedynym zabezpieczeniem:

  • UI: komponent <Gate cap="..."> renderuje element tylko, gdy zdolność jest przyznana.
  • Provider: hook useCapability(cap) w warstwie stanu.
  • Worker: isMessageAllowed(msg, role, mode) waliduje każdą komendę przed wykonaniem; odrzucenie zwraca command_rejected.

Zbiór zdolności (Capability, 28 pozycji) dzieli się na grupy: inject.* (niefizjologiczne wstrzyknięcia: tylko sandbox lub override instruktora), clinical.* (interwencje: lek, defibrylacja, NIBP, FiO₂, płukanie, RKO, intubacja), scenario.*, config.*, time.* oraz instructor.*.

Kilka reguł wartych podkreślenia, bo niosą sens dydaktyczny:

  • Student w trybie exam ma wyłącznie clinical.*, wszystkie panele wstrzyknięć, konfiguracji i scenariusza są ukryte.
  • Student w trybie exam nie może pauzować: timer sesji jest chroniony (time.pause zablokowane).
  • time.seek (przewijanie) jest dostępne tylko w sandbox.
  • Instruktor ma inject.* i instructor.*, ale nie clinical.*: interweniuje przez sterowanie sesją, nie zastępuje studenta przy łóżku.
  • Komenda sync_state jest dozwolona wyłącznie dla roli Display, co blokuje prymitywną eskalację uprawnień.

Egzamin ma checklistę oceniającą (ExamChecklistItem): wzorce dopasowania działań, okna czasowe i punktacja, plus dziennik akcji do debriefingu.

13Warstwa AI/RAG

Warstwa AI jest w całości opcjonalna i stanowi nadbudowę, a nie fundament. Deterministyczny rdzeń, czyli farmakodynamika (krzywa Hilla), trzynaście reguł kontraindykacji, harmonogram zdarzeń chorobowych na seedowanym generatorze i inwariant perfuzji, działa bez jednego wywołania modelu językowego. AI dokłada dwie rzeczy: semantyczne wyszukiwanie w bazie wiedzy klinicznej (RAG) oraz kilka generatywnych funkcji pomocniczych. Co ważne dla architektury: system rozdziela dwa niezależne połączenia AI, osobno model czatu (LLM) i osobno model embeddingów, każde z własnym adresem, kluczem i możliwością pracy lokalnej. Wszystkie fakty w tym rozdziale pochodzą z apps/api/src/services/{ai,knowledge,settings}.ts, routes/{ai,rag}.ts, db/schema/knowledge.ts oraz packages/core/src/engine/event-plausibility.ts.

13.1. Baza wiedzy: ingest z vaulta i chunking

Źródłem wiedzy jest vault Obsidian, ingestowany skryptem (scripts/ingest-vault.ts). Skrypt wciąga wyłącznie sześć folderów klinicznego ground-truth: 02-Parametry, 03-Arytmie-i-EKG, 04-Alarmy, 05-Scenariusze, 09-Algorytmy-syntezy i 99-Referencje. Reszta vaulta (plany, architektura, UI) zostaje dokumentacją tylko w Obsidianie, zgodnie z zasadą „jedna treść, jedno źródło". Przed chunkingiem usuwany jest frontmatter (stripFrontmatter), a pliki krótsze niż 50 znaków są pomijane. Skrypt ma tryby --dry-run (sama lista plików) i --reset (czyści knowledge_chunks i knowledge_sources, po czym ingestuje od zera).

Chunker (chunkText w services/knowledge.ts) jest świadomy granic zdań, a jego parametry są jawne:

  • rozmiar docelowy fragmentu ~2000 znaków (około 500 tokenów), z zakładką (overlap) ~200 znaków (około 50 tokenów),
  • cofnięcie cięcia do najbliższej granicy zdania (regex na [.!?] + spacja + wielka litera), by nie rozrywać zdań w połowie,
  • twarde zabezpieczenia: maksimum 5000 fragmentów na źródło, odrzucanie fragmentów krótszych niż 30 znaków, oraz gwarancja postępu pętli (anti-infinite-loop),
  • deduplikacja po md5(content) względem istniejących fragmentów: powtórki nie są ponownie embedowane,
  • embedowanie wsadowe po 100 fragmentów na wywołanie (EMBED_BATCH_SIZE), taniej i szybciej.

Metadane źródła (tytuł, URL, typ licencji, znaczniki) trafiają do knowledge_sources, a każdy fragment do knowledge_chunks z tagami ['vault', folder] i krótkim streszczeniem (extractSummary: nagłówek, pierwsze zdanie lub pierwsze 80 znaków). Usunięcie źródła kaskadowo usuwa jego fragmenty.

13.2. Model embeddingów i wymiary

Domyślnym modelem embeddingów jest bge-m3, 1024 wymiary, uruchamiany lokalnie przez LM Studio (resolveEmbeddingModel zwraca EMBEDDING_MODEL ?? 'bge-m3'). bge-m3 jest wielojęzyczny i dobrze obsługuje polski, co ma znaczenie, bo baza wiedzy jest po polsku. Embeddingi liczone są przez interfejs zgodny z OpenAI (POST /embeddings), więc ten sam kod działa z LM Studio lokalnie i z OpenAI w chmurze; różni je tylko adres bazowy.

Wymiar jest twardym kontraktem. Kolumna wektora ma ustalony rozmiar 1024, więc zmiana modelu embeddingów na inny wymiar (np. OpenAI text-embedding-3-small ma 1536) wymaga migracji schematu i ponownej ingestii całej bazy. Rozbieżność z przeszłości, gdzie domyślny literał wskazywał model 1536d niepasujący do kolumny 1024d, została usunięta migracją 0007_embedding_1024.sql (1536 → 1024).

13.3. Magazyn wektorów: pgvector i indeks HNSW

Magazynem jest rozszerzenie pgvector w PostgreSQL. Fragmenty wiedzy przechowują kolumnę embedding vector(1024) (NOT NULL), a nad nią zbudowany jest indeks przybliżonego wyszukiwania:

Indeks: HNSW (Hierarchical Navigable Small World) Operator: vector_cosine_ops (metryka kosinusowa) Parametry: m = 16, ef_construction = 64 (decyzja D-DB-03)

pgvector jest jedynym backendem RAG. Wcześniejszy komponent LightRAG (retrieval i generacja w jednym sidecarze) został porzucony 16 lipca 2026 jako kruchy, a cały jego kod, endpointy, UI i wpis w docker-compose usunięto. Ślad po nim to jedynie ignorowany parametr _mode w sygnaturze searchKnowledge, zachowany dla zgodności wywołań (dawne tryby mix/naive/local/global/hybrid już nie istnieją).

13.4. Wyszukiwanie semantyczne: kosinus, top-K, próg

Wyszukiwanie (searchKnowledge(query, topK = 5)) embeduje zapytanie tym samym modelem, po czym pyta pgvector operatorem odległości kosinusowej <=> i sortuje rosnąco po odległości:

SELECT kc.content, 1 - (kc.embedding <=> :queryVec) AS similarity FROM knowledge_chunks kc JOIN knowledge_sources ks ON ks.id = kc.source_id ORDER BY kc.embedding <=> :queryVec LIMIT :topK

Podobieństwo (similarity) mieści się w zakresie 0..1 (1 = identyczne). Próg odcięcia nie jest globalny, lecz dobierany per funkcja, bo różne zastosowania mają różną tolerancję na słaby dopasowanie:

  • doradca lekowy pobiera top-5 i odrzuca fragmenty o podobieństwie ≤ 0,50,
  • ekstraktor wpływu choroby i farmakodynamika leku pobierają top-3 z progiem > 0,45,
  • walidator zdarzeń pobiera top-10, a ocena sesji top-15, oba bez twardego progu (surowy top-K trafia do kontekstu).

Nie ma ponownego rankowania (rerank) ani wyszukiwania hybrydowego słowo plus wektor. Gdy żaden fragment nie przekroczy progu, kontekst RAG jest po prostu pomijany, a model odpowiada bez cytatów z bazy. To świadoma, ale istotna granica: retrieval jest prosty i zależny od jakości embeddingów zapytania.

13.5. Providerzy LLM i konfigurowalne endpointy

Model językowy podłączany jest przez interfejs zgodny z OpenAI (POST /chat/completions). Provider wynika wprost z adresu bazowego, więc ta sama warstwa obsługuje chmurę (OpenRouter) i tryb lokalny (LM Studio) bez zmian w kodzie. Adresy i klucze rozstrzygane są kaskadowo (services/settings.ts):

WarstwaKolejność adresu bazowegoDomyślny providerKlucz
Czat (LLM)DB → LLM_BASE_URL → OpenRouteropenrouter.ai/api/v1OPENROUTER (opcjonalny dla local)
EmbeddingiDB → EMBEDDING_BASE_URL → OpenAIapi.openai.com/v1OPENAI (opcjonalny dla local)

Rozpoznanie endpointu lokalnego jest regexowe (isLocalUrl: localhost, 127.0.0.1, host.docker.internal, sieci prywatne 192.168.x, 10.x, 172.16-31.x). Dla adresu lokalnego klucz nie jest wymagany, koszt wynosi zero, a dane nie opuszczają maszyny, co przy treściach klinicznych ma realne znaczenie. Domyślny model czatu to anthropic/claude-3.5-haiku; lokalnie sprawdzane są modele takie jak Bielik-11B czy Gemma, w chmurze rodzina Claude (Haiku/Sonnet/Opus) i Gemini.

Warstwa uwzględnia praktyczne różnice między providerami:

  • twardy limit czasu odpowiedzi: 240 s dla endpointu lokalnego (wolny model 11B), 120 s dla chmury, egzekwowany przez AbortController z czytelnym komunikatem po timeoucie,
  • LM Studio nie wspiera response_format: json_object (zwraca 400), więc dla local jest ono pomijane; JSON wymuszają wtedy same prompty, a parsery mają awaryjne domykanie nawiasów,
  • automatyczny upgrade modelu haiku → sonnet-4.6 (tylko na OpenRouter) dla zadań o ciężkim, strukturalnym JSON: farmakodynamika, ekstraktor chorób, kreator pacjenta,
  • na endpointcie lokalnym doradca lekowy dostaje skrócony katalog (bez pola notes) i niższy limit tokenów (900 zamiast 1500), by zmieścić się w rozsądnym czasie.

13.6. Funkcje korzystające z AI

Warstwa udostępnia osiem odrębnych zastosowań. Cztery pierwsze to funkcje pomocnicze przy łóżku lub w edytorze, cztery kolejne to narzędzia autorskie i oceniające. Kolumna „RAG" mówi, czy i jak funkcja dociąga kontekst z bazy wiedzy.

FunkcjaWywołanie w kodzieRAG (top-K, próg)Zabezpieczenie wyjścia
Doradca lekowy („Porada AI")drug_advisortop-5, > 0,50tylko leki z katalogu (whitelist kodów), max 6 sugestii
Farmakodynamika lekudrug_effecttop-3, > 0,45clamp delt (HR ±80, MAP ±60, SpO₂ −20..+5, ...)
Wpływ choroby na parametrydisease_impacttop-3, > 0,45clip offsetów do zakresów fizjologicznych
Generator scenariuszyscenario_builderbrakparser z domykaniem nawiasów, 1 retry
Ekstraktor chorób z tekstudisease_extractorbrakwalidacja enum rytmu, drugi przebieg guidance
Kreator pacjenta z narracjipatient_creatorbraknaprawa obciętego JSON (stos nawiasów)
Walidator zdarzeńvalidate-eventtop-10, bez proguwerdykt ok/warning/block, cache 24 h
Ocena sesji (debrief)grade-sessiontop-15, bez proguekstrakcja JSON, cache 24 h

Doradca lekowy (Porada AI) to najbardziej widoczna funkcja: przycisk „Porada AI" na monitorze otwiera modal (DrugAdvisorModal.tsx), który wysyła do /api/ai/drug-advisor migawkę stanu pacjenta (vitals, rytm, choroby ICD-10, leki stałe, alergie, aktywne alarmy) wraz z pełnym katalogiem leków z bazy. Model zwraca ocenę sytuacji i uszeregowane sugestie (first-line / second-line / consider / avoid) z dawką, drogą i uzasadnieniem, plus działania niefarmakologiczne i nazwę algorytmu (ACLS/ALS/ATLS). Kluczowy bezpiecznik: clipDrugAdvisor odrzuca każdą sugestię, której drugCode nie występuje w katalogu, więc model nie może „wymyślić" leku spoza formularza. Dodatkowo per-choroba przekazywany jest hint z bazy (treatmentGuidance: sugerowane kategorie i leki przeciwwskazane).

Generator scenariuszy działa jako czat wielokrotny w zakładce AI (AITab.tsx): model najpierw dopytuje o brakujące informacje (typ przypadku, pacjent, przebieg, cele), a gdy ma ich dość, zwraca gotowy scenariusz (stan wyjściowy plus oś czasu zdarzeń), który uczeń może uruchomić w silniku. Funkcje autorskie (ekstraktor chorób, kreator pacjenta) generują wpisy do bazy z pełną walidacją enum rytmów, kapnogramów i ścieżek parametrów, więc niepoprawne wartości od modelu są odrzucane, a nie zapisywane.

13.7. Uziemienie zdarzeń i walidacja plauzybilności

Granica „biologicznie możliwe / niemożliwe" jest egzekwowana dwuwarstwowo, z prymatem warstwy deterministycznej. Punktem kontrolnym jest obsługa add_event w workerze; przechodzą przez nią tylko zdarzenia wnoszące patologię (zmiana rytmu, zmiana ST, odma, anafilaksja, krwawienie, zaciśnięcie, wymuszona ekstubacja, nacięcie), a leki i akcje kliniczne są zwolnione.

  • Uziemienie deterministyczne (isEventGrounded): zdarzenie patologiczne jest dozwolone, gdy ma pokrycie w profilu chorobowym pacjenta. Dla zmiany rytmu oznacza to, że któraś z chorób pacjenta ma w swoich zdarzeniach ten właśnie rytm; powrót do rytmu zatokowego i normalizacja ST przechodzą zawsze.
  • Walidator RAG (POST /api/rag/validate-event): retrieval z pgvector (top-10) plus wywołanie LLM, który zwraca werdykt ok / warning / block i cytowania źródeł, parsowane ze sztywnego formatu VERDICT/REASON. Wynik jest cache'owany 24 h w pamięci.

Bramka trybowa (isEventAllowedInMode) łączy obie warstwy: werdykt block odrzuca zdarzenie, werdykt ok/warning je dopuszcza, a przy braku werdyktu decyduje uziemienie deterministyczne. W trybach scenario i story to właśnie uziemienie jest podstawową gwarancją; werdykt RAG jest opcjonalną nadbudową i w praktyce dołączany jest głównie na ścieżce override instruktora (InstructorControls.tsx), gdzie na zdarzeniu stemplowane jest pole validated tylko dla werdyktów ok/warning.

Uczciwie o walidatorze. Walidator RAG jest permisywny na awarię: gdy endpoint zawiedzie, zwraca warning z komunikatem „akcja dozwolona z ostrzeżeniem", a nie block. Nie blokuje więc prowadzącego przy problemie z infrastrukturą, ale też nie jest twardym filtrem bezpieczeństwa. Twardą regułą pozostaje deterministyczne uziemienie i macierz uprawnień (rozdz. 12).

Pokrewna funkcja to ocena sesji (POST /api/rag/grade-session), używana w debriefingu: na podstawie dziennika akcji, alarmów i punktacji model generuje narracyjny werdykt (excellent / good / acceptable / poor) z mocnymi stronami, obszarami poprawy i ryzykami, opierając się na wytycznych ERC/ESC/PTL/ASA i kontekście z bazy wiedzy (top-15).

13.8. Guardraile, cache i audyt

Ponieważ wyjście modelu jest z natury zawodne, każda funkcja ma warstwę zaciskania i naprawy, niezależną od tego, co model faktycznie zwróci:

  • Zaciskanie fizjologiczne jako bariera na prompt injection: clipPhysiology, clipDrugEffect, clipDrugAdvisor przycinają offsety i delty do zakresów bezpiecznych (np. delta HR do ±80, MAP do ±60), niezależnie od tego, jak duże wartości poda model.
  • Walidacja enumów: rytmy, kształty kapnogramu i ścieżki parametrów spoza dozwolonych zbiorów są odrzucane; normDrugTarget normalizuje przypadki, gdy model kopiuje literalnie enum („hr|mapMmHg|...").
  • Odporne parsowanie JSON: sanitizeModelJson usuwa komentarze i przecinki końcowe, a fallbacki wyciągają zbalansowany blok nawiasów lub domykają JSON obcięty limitem tokenów (śledzenie stosu {/[), co jest częste przy lokalnych modelach.

Każde wywołanie jest cache'owane i audytowane. ai_response_cache kluczuje po sha256(modelId | useCase | input) i zlicza trafienia, więc deterministyczny input nie płaci drugi raz. ai_queries to trwały dziennik: prompt i odpowiedź (przycięte do 20 000 znaków), tokeny wejścia i wyjścia, koszt w millicentach (1/100000 USD, liczony z tabeli cen per model; endpoint lokalny = 0), latencja, status cache oraz kod błędu. Błąd zapisu audytu jest niekrytyczny i nie blokuje samego AI.

13.9. Tryb bez AI: degradacja fail-closed

Wyłączenie AI degraduje system bezpiecznie, do w pełni deterministycznego rdzenia, a nie do stanu nieokreślonego. Zachowanie jest jawne:

  • brak klucza i brak endpointu lokalnego: complete() rzuca wyjątek, zamiast działać po cichu, a endpointy zwracają 503 ai_disabled lub 502 ai_failed z czytelnym komunikatem,
  • RAG na ścieżkach opcjonalnych (doradca, farmakodynamika, wpływ choroby) jest opakowany w obsługę błędu i po prostu pomijany: searchKnowledge bez klucza embeddingów zwraca pustą listę,
  • silnik kliniczny działa dalej: farmakodynamika (Hill), kontraindykacje, harmonogram zdarzeń chorobowych i inwariant perfuzji są deterministyczne i niezależne od dostępności modelu.

13.10. Ograniczenia i uczciwy rachunek AI

Warstwa AI jest przydatna, ale jej możliwości są celowo ograniczone, a granice warto nazwać wprost:

  • Brak autonomicznych decyzji klinicznych. AI wyłącznie doradza i generuje materiał; interwencje o skutkach fizjologicznych (defibrylacja, farmakodynamika, kontraindykacje) pozostają deterministyczne i niezależne od modelu. Doradca proponuje, uczeń decyduje.
  • Halucynacje. Zaciskanie i whitelisty ograniczają szkody (nierealne wartości są przycinane, leki spoza katalogu odrzucane), ale nie gwarantują poprawności merytorycznej pojedynczej odpowiedzi. Confidence zwracany przez model jest deklaracją, nie dowodem.
  • Prosty retrieval. Progi podobieństwa (0,45 / 0,50) są heurystyczne, brak rerankingu i wyszukiwania hybrydowego; poniżej progu kontekst jest cicho pomijany, co przy słabym zapytaniu obniża trafność cytowań.
  • Walidator plauzybilności jest opcjonalną nadbudową. Dziedziczy zawodność modelu, na awarię działa permisywnie (warning, nie block) i realnie spina się głównie z override instruktora; twardą gwarancją pozostaje uziemienie deterministyczne i macierz uprawnień.
  • Porzucony komponent. LightRAG został w całości usunięty (2026-07-16); jedynym backendem RAG jest pgvector, a wcześniejsza rozbieżność wymiaru embeddingów (1536 vs 1024) została naprawiona u źródła.

14Persystencja i baza danych

Backend (Fastify + Drizzle ORM + PostgreSQL) utrwala 22 tabele (apps/api/src/db/schema/). Kluczowe grupy:

  • Sesje i postęp: sessions (status running/paused/completed/aborted), session_events (dziennik komend), session_snapshots (okresowe migawki pełnego PatientState z alarmami, podstawa wznowienia i replayu).
  • Pacjent: patients plus tabele powiązane: choroby, leki przewlekłe, alergie, wyniki laboratoryjne, zabiegi.
  • Katalogi: drugs (parametry PK/PD w JSONB), drug_interactions, diseases (z physiologyImpact w JSONB).
  • Egzaminy: exam_templates (timeline + checklista), exam_sessions (dziennik akcji, status).
  • Wiedza i AI: knowledge_sources, knowledge_chunks (embedding 1024d), ai_response_cache, ai_queries.
  • Konfiguracja i konta: app_settings, users, organizations.

Sesja jest tworzona przy starcie, migawki zapisywane okresowo (preview: HR/MAP/SpO₂ oraz czas symulacji i osi zdarzeń), a wznowienie inicjuje stan z ostatniej migawki zamiast od zera. Rozszerzenie pgvector inicjowane jest w obrazie Dockera; migracje Drizzle numerowane są od 0000 do 0007 (ostatnia: zmiana wymiaru embeddingu 1536 → 1024).

15Jakość, testy i walidacja

Poprawność nie jest deklaracją, lecz zestawem testów. Wszystkie 26 rytmów przeszło wieloetapowy audyt fizjologiczny (morfologia EKG oraz spójność przebieg–liczba), a każdy wykryty defekt naprawiono i zabezpieczono testem regresyjnym. Pakiet liczy obecnie 196 testów silnika (fizjologia, alarmy, farmakodynamika, rytmy, macierz uprawnień), a wszystkie pakiety przechodzą statyczną kontrolę typów w trybie ścisłym.

Wśród utrwalonych inwariantów są między innymi:

  • inwariant braku perfuzji: żaden rytm bez tętna nie pojawia się z „żywą" wartością SpO₂ czy ciśnienia,
  • poprawna progresja odstępów R–R w bloku Wenckebacha z zablokowanym załamkiem P,
  • widoczny kolec stymulacji komorowej przy dowolnej częstości,
  • zanik amplitudy migotania komór od grubo- do drobnofalistego,
  • zatokowa arytmia oddechowa sprzężona z rzeczywistym torem oddechu.

Sposób pracy, najpierw audyt, potem test pod każdy defekt, jest bezpośrednim przełożeniem zasady z rozdziału 1: skoro liczy się spójność, to właśnie spójność jest testowana, nie wygląd pojedynczej krzywej.

16Źródła wiedzy klinicznej

Podział między kodem a wiedzą jest świadomy. Kod trzyma tylko „maszynowe profile napędzające parametry"; patofizjologia, tabele etapów, cytowania i normy mieszkają w vaulcie Obsidian i są stamtąd ingestowane do bazy wektorowej. Kod odsyła do vaulta przez pola sourceRef.

Źródła, na których opiera się warstwa kliniczna:

  • Dokumentacja producentów monitorów: Philips IntelliVue (MP/MX), GE Carescape/Dash, Mindray, Dräger; stąd konwencje prezentacji (np. odświeżanie numeryki 1 Hz, morfologie fal).
  • Normy: IEC 60601-1-8 (alarmy), IEC 60601-2-27 (EKG), ISO 80601-2-55 (kapnografy), ISO 80601-2-61 (pulsoksymetry) i pokrewne.
  • Wytyczne kliniczne: ERC 2021 (resuscytacja), ESC (STEMI), czwarta uniwersalna definicja zawału, ATLS (klasy krwotoku).
  • Modele syntezy sygnału EKG: prace nad dynamicznym modelem EKG (suma funkcji Gaussa) i pochodne.
  • Bazy sygnałów fizjologicznych: PhysioNet (MIT-BIH, PTB, LUDB) jako odniesienie morfologiczne.
  • Katalog chorób i leków w bazie z ładunkiem physiologyImpact, częściowo walidowanym przez AI (pole source rozróżnia wpis ręczny, AI i przegląd ekspercki).

Dokładne dane bibliograficzne tych pozycji zebrano w rozdziale 18 z adnotacją o konieczności weryfikacji.

17Ograniczenia, dług techniczny i dalszy rozwój

Uczciwy rachunek granic tego, co system osiągnął.

  • Brak walidacji na użytkownikach. Wszystkie twierdzenia o jakości dotyczą wierności wewnętrznej modelu, potwierdzonej audytem eksperckim i 196 testami, a nie efektu dydaktycznego mierzonego na studentach. To najważniejsze ograniczenie i pierwszy kierunek dalszych prac.
  • Walidacja morfologii bez formalnego porównania z bazą zapisów. Audyt rytmów był ekspercki; ilościowe porównanie przebiegów z publicznymi bazami sygnału (PhysioNet) wzmocniłoby dowód wierności.
  • Dług techniczny, zidentyfikowany i usunięty (2026-07-16). Trzy rozbieżności, wszystkie naprawione: (1) domyślny wymiar embeddingów wyrównany do schematu pgvector (1024d, bge-m3); (2) porzucony LightRAG usunięty w całości; (3) nieaktualne notatki architektury v1 (OffscreenCanvas) sprostowane.
  • Zakres modelowania. System obejmuje stanowisko monitorowania anestezjologicznego, ale nie sięga jeszcze pełnej maszyny do znieczulenia: parownika, obiegu gazów, wentylacji mechanicznej. To naturalny kierunek rozbudowy.
  • Zależność warstwy plauzybilności od modelu językowego. Granica „biologicznie możliwe / niemożliwe" bywa nieostra; walidator RAG dziedziczy zawodność modelu, dlatego pozostaje opcjonalną nadbudową nad regułami deterministycznymi, a nie ich zamiennikiem.

18Referencje

18.1. Źródła kodu (weryfikowalne w repozytorium)

Wszystkie dane techniczne, wzory i stałe w tym dokumencie pochodzą z repozytorium (commit 8ee2402, wersja 0.3.1). Kluczowe pliki: packages/core/src/synth/* (synteza sygnałów), physiology-engine.ts (model fizjologii), utils/perfusion.ts (inwariant perfuzji), data/{clinical-events,arrest-model}.ts (zdarzenia i NZK), pharma/drug-pd.ts i contraindications.ts (farmakodynamika i kontraindykacje), engine/{alarm-engine,defibrillator,event-plausibility}.ts, capabilities.ts (macierz uprawnień), apps/api/src/db/schema/* (baza), services/{ai,knowledge}.ts (warstwa AI).

18.2. Źródła zewnętrzne: do zweryfikowania

Poniższe pozycje przywołano z pamięci i należy potwierdzić ich dane bibliograficzne przed publikacją:

  • IEC 60601-1-8: norma alarmów w elektrycznym sprzęcie medycznym (podać wydanie).
  • IEC 60601-2-27: wymagania dla monitorowania EKG.
  • ISO 80601-2-55: kapnografy; ISO 80601-2-61: pulsoksymetry.
  • Wytyczne resuscytacji ERC 2021: rozdział zaawansowanych zabiegów (ALS) i defibrylacji.
  • Czwarta uniwersalna definicja zawału (2018): kryteria uniesienia ST per terytorium.
  • Wytyczne ESC dotyczące STEMI: postępowanie w ostrym zespole wieńcowym z uniesieniem ST.
  • ATLS: klasy krwotoku I–IV.
  • McSharry P.E., Clifford G.D. i wsp.: dynamiczny model syntezy EKG, IEEE Trans. Biomed. Eng. (ok. 2003), zweryfikować rok i tom.
  • PhysioNet: bazy sygnałów MIT-BIH, PTB, LUDB (podać cytowanie).
  • Dokumentacja techniczna monitorów Philips IntelliVue i GE Carescape, podać wydania przewodników.

Dodatkowe pozycje z zakresu efektywności symulacji medycznej (ramowy model Jeffries, badanie NCSBN, metaanalizy Cook i wsp.) są istotne dla ewentualnego rozdziału o walidacji dydaktycznej (rozdz. 17), ich dane bibliograficzne również wymagają weryfikacji.

Zastrzeżenie

Kardo to narzędzie wyłącznie edukacyjne i szkoleniowe. Nie jest wyrobem medycznym w rozumieniu rozporządzenia MDR (UE) 2017/745 i nie może być używane do diagnostyki, monitorowania ani decyzji klinicznych u rzeczywistych pacjentów.