Hospital-at-Home Risk Monitor Contracts
이 문서는 Hospital-at-Home Risk Monitor 예제에서 언어와 app 사이를 연결하는 공통 계약을 정의합니다. 각 언어별 문서는 구현 책임을 설명하고, 이 문서는 서로 어떤 데이터와 protocol로 맞물리는지 설명합니다.
이 contract는 상용 제품 claim이 아니라 dev-guide 최소 실행 예제를 위한 engineering contract입니다. 실제 제품에서는 병원 EMR, device firmware, model validation, regulatory documentation에 맞춰 별도 검증이 필요합니다.
처음 읽을 때는 모든 JSON 예시를 한 번에 외우려고 하지 않아도 됩니다. 먼저 아래 순서로 contract의 큰 경계만 잡고, 실제 payload shape는 schema와 fixture를 함께 확인합니다.
| 먼저 볼 것 | 이유 |
|---|---|
## 1. Contract 원칙 |
schema, fixture, protocol adapter, PHI boundary가 어떤 관계인지 잡습니다. |
## 2. Signal Packet v1 |
local demo와 device ingestion의 기본 입력을 이해합니다. |
## 4. Preprocessed Signal v1 |
Rust preprocessing이 Python risk workflow에 넘기는 feature contract를 봅니다. |
## 5. Risk Assessment v1와 ## 7. Monitoring Snapshot v1 |
Python API와 TypeScript dashboard가 공유하는 runtime contract를 봅니다. |
## 8. RiskMonitor Runtime Policy v1 |
Go operator와 CD repo가 바라보는 runtime desired/observed state를 봅니다. |
## 11. Validation Responsibility |
각 언어와 adapter가 어디서 contract를 검증해야 하는지 확인합니다. |
1. Contract 원칙
1-1. 예제 범위를 작게 유지한다
이 contract는 아래 local demo를 연결하는 데 필요한 최소 계약만 정의합니다.
synthetic signal generator
-> HTTP /signal-packets ingestion in local demo
-> Python API
-> app-owned preprocessing adapter
-> Python core clinical result builder
-> TypeScript dashboard
HTTP POST /signal-packets는 1차 local demo와 CI contract test의 기본 입력 경로입니다. MQTT는 같은 domain contract와 usecase를 호출하는 후속 inbound adapter로 둡니다.
아래 항목은 1단계 예제 범위가 아닙니다.
- 실제 PCB hardware driver
- HIL 테스트
- production MQTT security 전체 구현
- 실제 모델 학습 pipeline
- 병원 EMR/PACS/LIS 연동
- production-grade Kubernetes operator
- KubeVirt 기반 device VM
- Rust preprocessing network service
- Node API/BFF
- protobuf/Arrow 기반 binary transport
1-2. Domain language를 먼저 고정한다
언어별 type 이름이 달라도 제품 언어는 같아야 합니다. 이 예제에서 공통으로 사용하는 핵심 개념은 아래와 같습니다.
| Concept | Meaning |
|---|---|
SignalSample |
장비 또는 simulator가 특정 시점에 생성한 생체신호 sample |
SignalPacket |
여러 sample과 device metadata를 묶은 전송 단위 |
SignalFrame |
3600Hz 이상 waveform과 multi-channel sensor를 담는 channelized frame |
SignalFrameSet |
같은 site/ward/window에서 함께 처리할 수 있는 여러 SignalFrame 묶음 |
SignalProcessingRun |
SignalFrameSet을 feature로 변환하는 실행 단위와 그 provenance |
PreprocessedSignal |
Rust preprocessing 이후 Python model이 소비할 feature contract |
ClinicalContext |
risk assessment에 필요한 synthetic clinical metadata |
RiskAssessment |
Python risk usecase가 만든 위험도 결과 |
ReviewQueueItem |
TypeScript dashboard가 표시할 의료진 검토 단위 |
AlarmAuditEvent |
acknowledge/mute 같은 alarm workflow action 이력 |
RiskMonitor |
Go minimal operator가 감시하는 Kubernetes custom resource |
RuntimePolicy |
RiskMonitor spec/status로 표현되는 model/runtime/threshold 정책 |
FirmwareReleaseManifest |
.elf/.hex/.bin/.map firmware release artifact를 source revision과 checksum으로 묶는 evidence index |
1단계 초반에는 이 문서의 설명과 아래 JSON Schema가 contract source of truth입니다. 문서는 사람이 읽는 의도와 경계를 설명하고, schema는 app/package 사이 payload shape를 machine-readable contract로 고정합니다.
contracts/hospital-at-home/
signal-frame.v1.schema.json
signal-frame-set.v1.schema.json
signal-packet.v1.schema.json
preprocessed-signal.v1.schema.json
risk-assessment.v1.schema.json
review-queue-item.v1.schema.json
monitoring-snapshot.v1.schema.json
alarm-audit-event.v1.schema.json
runtime-policy.v1.schema.json
firmware-release-manifest.v1.schema.json
Schema 파일의 examples, fixtures/signals/ CSV, manifests/risk-monitor.demo.json은 make contract/test로 검증합니다. 문서의 JSON 예시는 schema example과 일치해야 합니다.
각 언어 테스트는 최소 하나 이상의 fixture 또는 schema를 참조해야 합니다.
| Area | Contract check |
|---|---|
| C/Firmware | SignalPacket/SignalFrame equivalent fixture와 packet build test |
| Rust | SignalPacket/SignalFrameSet/window fixture -> PreprocessedSignal |
| Python | OpenAPI/JSON schema compatible request/response |
| TypeScript | ReviewQueueItem fixture -> view model mapper |
| Go | RiskMonitor manifest fixture -> Go type/reconcile test |
| Firmware release | FirmwareReleaseManifest schema -> .elf/.hex/.bin/.map release evidence manifest |
1-3. Protocol은 adapter다
MQTT, HTTP, file fixture, Kubernetes API는 domain이 아닙니다. Domain과 usecase는 protocol을 몰라야 하고, adapter가 protocol payload를 domain input으로 변환합니다.
MQTT payload / HTTP body / CSV fixture
-> adapter
-> domain contract
-> usecase
1-4. PHI를 포함하지 않는다
dev-guide fixture와 demo message에는 실제 환자 PHI를 포함하지 않습니다. patientId 대신 synthetic subjectId 또는 demoSubjectId를 사용합니다. 실제 제품에서는 PHI boundary, audit, encryption, retention policy를 별도 설계해야 합니다.
2. Signal Packet v1
SignalPacket은 1차 demo와 HTTP smoke test를 위한 row-oriented JSON 입력입니다. 실제 PCB 장비에서 PPG, EMG, motion, impedance 같은 channel이 초당 수천 point 이상으로 들어오는 경우에는 JSON row array를 source of truth로 삼지 않습니다. 이때는 SignalFrame을 사용해 channel별 sampling rate, sample count, encoding, payload reference를 분리하고, raw waveform은 binary segment store나 equivalent raw segment boundary에 둡니다.
정리하면 1차 경계는 아래와 같습니다.
| Contract | Use |
|---|---|
SignalPacket |
local demo, HTTP contract test, small fixture |
SignalFrame |
high-frequency waveform, multi-channel frame, binary segment reference |
SignalFrameSet |
ward/gateway가 같은 processing window로 묶은 multi-subject/multi-device input |
PreprocessedSignal |
Rust preprocessing output consumed by Python risk workflow |
Redis는 최신 상태, short-lived stream, cache에는 사용할 수 있지만 raw waveform source of truth로 두지 않습니다. MQTT를 붙이면 MQTT inbound adapter가 SignalPacket, SignalFrame, 또는 SignalFrameSet domain input으로 변환하고, Rust preprocessing은 이 입력을 feature로 바꿉니다.
2-0. Grouped signal processing
여러 bed/device에서 같은 시간 창의 신호가 들어오는 경우 domain contract에는 Batch라는 이름을 쓰지 않습니다. batch size는 app runtime이 처리량과 지연시간을 조절하기 위한 실행 정책이고, clinical/domain contract가 아닙니다.
이 예제에서는 여러 frame을 함께 처리하는 입력을 SignalFrameSet으로 부릅니다.
SignalFrameSet
signalSetId
siteId
wardId
windowStartMs
windowEndMs
source
frames[]
SignalFrameSet은 하나의 환자만 의미하지 않습니다. 같은 ward/gateway/window에서 묶을 수 있는 여러 SignalFrame을 담습니다. 단건 처리는 크기 1인 SignalFrameSet으로 표현할 수 있습니다. App runtime은 내부적으로 frame을 묶을 때 maxItems, maxLatencyMs, maxPointsPerSet 같은 SignalGroupingPolicy를 사용할 수 있지만, 이 값은 Rust/Python domain result의 clinical 의미를 바꾸지 않아야 합니다.
SignalFrame의 channel payload는 두 경로를 갖습니다.
| Payload | Use |
|---|---|
payload |
fixture, small demo, contract smoke |
payloadRef |
high-rate waveform segment, product-like ingestion |
payloadRef는 단순 문자열이 아니라 uri, binary encoding, byteLength, checksum, sampleOffset을 갖는 value object입니다. Python app과 Rust adapter는 이 metadata를 사용해 waveform 본문을 다시 읽을 수 있지만, core clinical workflow는 table 이름, object store 이름, Redis key 같은 storage detail을 알지 않습니다.
20 bed 이상을 처리하는 경로에서는 partial failure가 정상 흐름입니다. 한 frame의 quality가 낮거나 payload가 누락되어도 전체 set을 실패시키지 않고 item별 결과를 분리합니다.
SignalProcessingRunResult
processingRunId
signalSetId
status: completed | completed_with_errors | failed
succeededCount
failedCount
skippedCount
items[]
Idempotency와 retry를 위해 최소한 아래 identity가 contract에 남아야 합니다.
| Field | Reason |
|---|---|
signalSetId |
grouped processing input identity |
frameId |
per-device/per-subject frame identity |
deviceId |
hardware/source identity |
demoSubjectId or subject binding |
PHI 없는 subject binding |
sessionId |
device/bed/subject binding period |
windowStartMs, windowEndMs |
event-time processing window |
sequenceStart |
device-local ordering hint |
payloadFingerprint |
duplicate, retry, and projection idempotency |
2-1. SignalSample
SignalSample은 firmware/simulator가 생성하는 최소 sample입니다.
{
"offsetMs": 0,
"sequenceNumber": 128,
"ppgRaw": 512,
"ecgRaw": 23,
"spo2Permille": 980,
"heartRateBpm": 72,
"motionMg": 12,
"contactQuality": 98
}
필드 기준은 아래와 같습니다.
| Field | Type | Required | Meaning |
|---|---|---|---|
offsetMs |
integer | yes | packet 기준 상대 시각 |
sequenceNumber |
integer | yes | device-local sequence number |
ppgRaw |
integer | no | raw or lightly processed PPG sample |
ecgRaw |
integer | no | raw or lightly processed ECG sample |
spo2Permille |
integer | no | SpO2를 permille로 표현. 980은 98.0% |
heartRateBpm |
integer | no | device-local heart rate estimate |
motionMg |
integer | no | accelerometer 기반 motion magnitude |
contactQuality |
integer | no | 0-100 contact quality |
2-2. SignalPacket
SignalPacket은 HTTP ingestion 또는 후속 MQTT adapter로 전송되는 단위입니다.
{
"schemaVersion": "signal.packet.v1",
"siteId": "demo-site",
"deviceId": "demo-device-001",
"demoSubjectId": "demo-subject-001",
"timestampMs": 1718600000000,
"samplingRateHz": 50,
"firmware": {
"version": "0.1.0-dev",
"hardwareRevision": "demo"
},
"quality": {
"droppedSamples": 0,
"sensorDisconnected": false,
"clipped": false,
"saturated": false,
"excessiveMotion": false
},
"samples": [
{
"offsetMs": 0,
"sequenceNumber": 128,
"ppgRaw": 512,
"ecgRaw": 23,
"spo2Permille": 980,
"heartRateBpm": 72,
"motionMg": 12,
"contactQuality": 98
}
]
}
필수 metadata는 아래와 같습니다.
| Field | Meaning |
|---|---|
schemaVersion |
payload schema version |
siteId |
demo site 또는 병원 site scope |
deviceId |
device identity |
demoSubjectId |
PHI가 아닌 synthetic subject identity |
timestampMs |
packet 기준 Unix epoch milliseconds |
samplingRateHz |
sample rate |
firmware.version |
firmware artifact version |
firmware.hardwareRevision |
hardware revision 또는 demo |
quality |
hardware/device 관점 quality flag |
samples |
packet에 포함된 sample list |
2-3. Ordering과 duplicate 기준
Device ingestion은 network 상태에 따라 중복이나 순서 변경이 발생할 수 있습니다. 1단계 demo에서는 아래 기준만 문서화하고, 구현은 duplicate detection과 missingness 반영까지만 목표로 합니다.
(deviceId, sequenceNumber)는 duplicate detection의 기본 key입니다.timestampMs + offsetMs는 분석용 event time입니다.- missing sequence는
droppedSamples또는 Rust preprocessing의 missingness로 반영합니다. - 같은 sequence가 중복 수신되면 ingestion adapter는 idempotent하게 처리합니다.
3. MQTT Contract
3-1. Topic naming
MQTT는 HTTP와 같은 app workflow를 호출하는 inbound adapter입니다. Topic은 site와 source scope를 드러내고, broker URL, credential, QoS, reconnect는 app runtime config가 소유합니다.
patient-risk/{site_id}/{device_id}/signal-packets/v1
patient-risk/{site_id}/{ward_id}/signal-frame-sets/v1
| Topic | Payload |
|---|---|
patient-risk/{site_id}/{device_id}/signal-packets/v1 |
SignalPacket |
patient-risk/{site_id}/{ward_id}/signal-frame-sets/v1 |
SignalFrameSet |
3-2. QoS 기준
MQTT adapter를 추가할 때는 아래 기준을 둡니다. Local demo에서는 Mosquitto 같은 broker를 사용할 수 있지만, production broker 운영과 보안 hardening은 구현하지 않습니다.
| Message | QoS | Reason |
|---|---|---|
| signal packet | 1 | 최소 1회 전달. duplicate는 sequence로 처리 |
| signal frame set | 1 | 최소 1회 전달. duplicate는 signalSetId/payloadFingerprint로 처리 |
| device status | 1 | 상태 변경 누락 방지 |
| transient debug event | 0 | 유실 허용 |
Retained message는 device status에는 사용할 수 있지만 signal packet에는 사용하지 않습니다.
3-3. Adapter 책임
MQTT subscriber는 아래를 담당합니다.
- topic에서
siteId,deviceId추출 - payload schema validation
- topic scope와 payload scope 일치 확인
- duplicate 처리
- domain
SignalPacket,SignalFrame, 또는SignalFrameSet으로 변환 - invalid payload audit event 생성
Domain과 usecase는 MQTT topic, QoS, broker URL을 알지 않습니다.
3-4. Security 기준
1단계 예제는 local demo 중심이므로 production security를 구현하지 않습니다. 그래도 경계는 문서화합니다.
- broker URL과 credential은 app config 또는 secret으로 주입합니다.
deviceId만으로 device identity를 신뢰하지 않습니다.- MQTT topic의
siteId/deviceId와 payload의 scope를 비교합니다. - production에서는 TLS와 device credential rotation이 필요합니다.
- local demo에서는 plain broker를 허용합니다.
4. Preprocessed Signal v1
4-1. Rust preprocessing output
Rust는 SignalPacket, SignalFrame, 또는 SignalFrameSet을 받아 Python risk model이 소비할 feature를 만듭니다. 단건 API가 필요한 경우에도 domain에서는 크기 1인 SignalFrameSet으로 볼 수 있어야 합니다. Rust/PyO3 경계는 한 frame씩 반복 호출하는 구조보다 set 단위 호출을 우선합니다.
{
"schemaVersion": "preprocessed.signal.v1",
"deviceId": "demo-device-001",
"demoSubjectId": "demo-subject-001",
"preprocessingVersion": "rust-signal-preprocessor-0.1.0",
"windowStartMs": 1718600000000,
"windowEndMs": 1718600030000,
"sampleCount": 1500,
"missingRatio": 0.01,
"artifactScore": 0.12,
"signalQuality": "good",
"features": {
"heartRateMeanBpm": 78.5,
"heartRateTrendBpmPerMin": 2.1,
"spo2MinPermille": 945,
"spo2DropCount": 2,
"motionBurdenRatio": 0.08,
"baselineDeviationScore": 0.18
}
}
Multi-frame processing 결과는 전체 실행 결과와 item 결과를 분리합니다.
{
"schemaVersion": "signal.processing-run-result.v1",
"processingRunId": "processing-run-demo-001",
"signalSetId": "signal-set-demo-001",
"status": "completed_with_errors",
"succeededCount": 18,
"failedCount": 1,
"skippedCount": 1,
"items": [
{
"frameId": "frame-demo-001",
"deviceId": "demo-device-001",
"demoSubjectId": "demo-subject-001",
"status": "succeeded",
"preprocessedSignal": {
"schemaVersion": "preprocessed.signal.v1",
"deviceId": "demo-device-001",
"demoSubjectId": "demo-subject-001",
"preprocessingVersion": "rust-signal-preprocessor-0.1.0",
"windowStartMs": 1718600000000,
"windowEndMs": 1718600030000,
"sampleCount": 1500,
"missingRatio": 0.01,
"artifactScore": 0.12,
"signalQuality": "good",
"features": {
"heartRateMeanBpm": 78.5,
"heartRateTrendBpmPerMin": 2.1,
"spo2MinPermille": 945,
"spo2DropCount": 2,
"motionBurdenRatio": 0.08,
"baselineDeviationScore": 0.18
}
}
}
]
}
4-2. Rust와 firmware quality 차이
Firmware quality는 hardware integrity를 표현합니다. Rust quality는 분석 가능한 signal인지 표현합니다. 둘 다 필요하고 서로 대체하지 않습니다.
| Layer | Example |
|---|---|
| Firmware quality | clipped, saturated, sensor disconnected, dropped samples |
| Rust quality | artifact score, missing ratio, baseline deviation, physiological plausibility |
5. Risk Assessment v1
5-1. Python risk output
Python risk usecase는 preprocessing 결과, clinical context, model metadata, risk policy를 조합해 RiskAssessment를 반환합니다.
{
"schemaVersion": "risk.assessment.v1",
"assessmentId": "assessment-demo-001",
"demoSubjectId": "demo-subject-001",
"riskLevel": "high",
"riskScore": 0.82,
"riskHorizon": "24h",
"reviewRequired": true,
"recommendedAction": "clinician-review",
"evidence": [
{
"code": "spo2-drop",
"label": "Repeated SpO2 drop",
"severity": "high"
},
{
"code": "tachycardia-trend",
"label": "Rising heart rate trend",
"severity": "moderate"
}
],
"dataQuality": {
"signalQuality": "good",
"missingRatio": 0.01,
"artifactScore": 0.12
},
"auditContext": {
"modelVersion": "demo-risk-model-0.1.0",
"policyVersion": "demo-policy-0.1.0",
"preprocessingVersion": "0.1.0-dev",
"requestId": "req-demo-001"
}
}
5-2. Risk level 기준
초기 risk level은 demo policy입니다. 실제 임상 claim이 아닙니다.
| Level | Meaning |
|---|---|
low |
routine monitoring |
moderate |
trend review may be useful |
high |
clinician review required |
urgent |
urgent review workflow example |
6. Review Queue v1
6-1. TypeScript view input
TypeScript web app은 RiskAssessment를 view model로 변환해 review queue를 표시합니다.
{
"reviewItemId": "assessment-demo-001",
"demoSubjectId": "demo-subject-001",
"priority": "high",
"riskLabel": "High",
"riskScoreText": "0.82",
"primaryEvidence": [
"Repeated SpO2 drop",
"Rising heart rate trend"
],
"dataQualityLabel": "Good signal quality",
"modelVersion": "demo-risk-model-0.1.0",
"lastUpdatedMs": 1718600031000,
"stale": false
}
Frontend는 risk rule을 재계산하지 않습니다. API response를 읽기 쉬운 view model로 변환하고, loading/error/empty/stale state를 표현합니다.
7. Monitoring Snapshot v1
7-1. TypeScript runtime input
TypeScript web app은 review queue만 표시하는 화면에서 끝나지 않습니다. Python API는 UI가 waveform, numeric, risk, alarm을 한 번에 구성할 수 있는 MonitoringSnapshot을 제공해야 합니다. 이 snapshot은 frontend가 risk rule이나 alarm severity를 재계산하지 않도록 만든 runtime contract입니다.
{
"schemaVersion": "monitoring.snapshot.v1",
"demoSubjectId": "demo-subject-001",
"timestampMs": 1718600031000,
"waveforms": [
{
"code": "ppg",
"label": "PPG",
"sampleRateHz": 50,
"unit": "raw",
"windowStartMs": 1718600000000,
"samples": [512, 534, 548, 531]
}
],
"numerics": [
{
"code": "heart_rate",
"label": "Heart rate",
"value": 92,
"unit": "bpm",
"status": "watch"
},
{
"code": "spo2",
"label": "SpO2",
"value": 94.5,
"unit": "%",
"status": "warning"
}
],
"risk": {
"assessmentId": "assessment-demo-001",
"riskLevel": "high",
"riskScore": 0.82,
"riskHorizon": "24h"
},
"alarm": {
"alarmId": "alarm-demo-001",
"severity": "warning",
"code": "spo2_drop_risk",
"message": "SpO2 trend requires clinician review",
"audible": false,
"acknowledged": false,
"muted": false
},
"dataQuality": {
"signalQuality": "good",
"missingRatio": 0.01,
"artifactScore": 0.12
},
"auditContext": {
"modelVersion": "demo-risk-model-0.1.0",
"policyVersion": "demo-policy-0.1.0",
"preprocessingVersion": "0.1.0-dev",
"requestId": "req-demo-001"
}
}
7-2. Waveform과 numeric 기준
Waveform은 chart library의 prop shape가 아니라 product contract입니다. Backend는 어떤 생체신호인지, sample rate가 무엇인지, 어떤 window를 보고 있는지 전달하고, frontend는 이를 viewport와 rendering state로 변환합니다.
Numeric measurement도 단순 number가 아닙니다. status는 정상/관찰/경고/위급 상태를 표현하며, frontend는 이 값을 기준으로 시각적 강조를 바꿉니다. Frontend가 threshold를 다시 계산하지 않습니다.
| Field | Responsibility |
|---|---|
waveforms[].code |
waveform identity |
waveforms[].sampleRateHz |
rendering and time-axis scale |
waveforms[].samples |
small demo window data |
numerics[].code |
measurement identity |
numerics[].value |
display value |
numerics[].status |
visual state chosen by backend/domain policy |
7-3. Alarm 기준
Alarm은 risk level과 별도 contract로 둡니다. Risk가 높아도 알람이 항상 울리는 것은 아니며, 알람은 acknowledge, mute, snooze 같은 workflow state를 갖습니다.
| Field | Meaning |
|---|---|
severity |
normal, watch, warning, critical |
audible |
frontend가 sound request를 표시해야 하는지 |
acknowledged |
사용자가 확인했는지 |
muted |
현재 sound가 mute 상태인지 |
code |
alarm policy identifier |
Browser sound 재생 가능 여부는 frontend runtime state입니다. Backend는 audible intent를 결정하고, frontend는 autoplay policy와 사용자 설정에 따라 실제 sound state를 관리합니다.
Alarm action은 snapshot을 다시 계산하지 않고 현재 alarm state를 전이합니다. POST /alarms/{alarm_id}/ack는 acknowledged=true, audible=false로 갱신합니다. POST /alarms/{alarm_id}/mute는 muted=true, audible=false로 갱신하며 기존 acknowledged state를 보존합니다. 두 endpoint 모두 갱신된 MonitoringSnapshot을 반환합니다. GET /alarms/{alarm_id}/audit-events는 acknowledge/mute action이 만든 append-only audit event를 읽는 endpoint이며, 이전 alarm state와 현재 alarm state를 함께 반환합니다. 동일 action이 반복되어 alarm state가 바뀌지 않은 경우에도 사용자 시도는 audit event로 남기되, outcome=no-op으로 표시해 실제 상태 전이와 구분합니다.
8. RiskMonitor Runtime Policy v1
8-1. Minimal CRD contract
Go 예제는 RiskMonitor custom resource를 감시하는 minimal Kubernetes operator입니다. Production-grade operator가 아니라, spec/status와 reconcile loop를 보여주는 예제입니다. Machine-readable contract는 contracts/hospital-at-home/risk-monitor.v1.schema.json와 contracts/hospital-at-home/risk-monitor-status.v1.schema.json에 둡니다.
최소 CRD shape는 아래와 같습니다.
apiVersion: samd.tirosh.ai/v1alpha1
kind: RiskMonitor
metadata:
name: hospital-at-home-risk-monitor
generation: 7
annotations:
samd.tirosh.ai/runtime-workload-name: patient-risk-python-api
spec:
siteId: demo-site
desiredModelVersion: demo-hospital-at-home-risk-24h
desiredImageTag: 0.1.0-dev.42
desiredPolicyVersion: demo-policy-0.1.0
desiredPreprocessingVersion: rust-signal-preprocessor-0.1.0
rolloutChannel: dev
highRiskThreshold: 0.85
runtimeState:
observedModelVersion: demo-hospital-at-home-risk-24h
observedImageTag: 0.1.0-dev.41
observedPolicyVersion: demo-policy-0.1.0
observedPreprocessingVersion: rust-signal-preprocessor-0.0.9
healthy: true
runtimeState는 실제 CRD spec에 들어갈 production desired state가 아니라, 이 sample app이 Kubernetes API server 없이 reconcile을 재현하기 위해 주입받는 observed state입니다. 실제 controller-runtime adapter에서는 runtimeState를 CRD에 쓰지 않고 runtime 조회 adapter가 observed state를 만듭니다. 현재 sample의 KubernetesDeploymentRuntimeStateProvider는 metadata.annotations["samd.tirosh.ai/runtime-workload-name"]가 가리키는 Deployment를 읽어 annotation, first container image tag, Available condition을 RiskMonitorRuntimeState로 변환합니다. Controller는 이렇게 계산된 상태를 status.observedGeneration, status.driftDetected, status.conditions로 status subresource에 기록합니다.
Operator reconcile은 아래까지만 담당합니다.
RiskMonitor.specvalidation- synthetic/current runtime state와 desired state 비교
ReconcileDecision계산status.observedGeneration,status.driftDetected,status.conditions갱신
Operator가 담당하지 않는 것은 아래와 같습니다.
- 실제 model server rollout
- Argo CD/Application 조작
- admission/conversion webhook
- multi-cluster reconcile
- production HA controller
{
"schemaVersion": "runtime.policy.v1",
"riskMonitorName": "hospital-at-home-demo",
"siteId": "demo-site",
"desired": {
"modelVersion": "demo-risk-model-0.1.0",
"preprocessingVersion": "0.1.0-dev",
"policyVersion": "demo-policy-0.1.0",
"rolloutChannel": "dev"
},
"current": {
"modelVersion": "demo-risk-model-0.0.9",
"preprocessingVersion": "0.1.0-dev",
"policyVersion": "demo-policy-0.0.9",
"healthy": true
}
}
RiskMonitorStatus는 drift와 필요한 action을 표현합니다. conditions는 Kubernetes status summary로 사용하고, drifts는 field-level desired/observed/action detail로 둡니다.
{
"phase": "Drifted",
"driftDetected": true,
"requeueRequired": true,
"recommendedAction": "rollout-runtime",
"drifts": [
{
"field": "desiredImageTag",
"desired": "0.1.0-dev.42",
"observed": "0.1.0-dev.41",
"recommendedAction": "rollout-runtime"
},
{
"field": "desiredPreprocessingVersion",
"desired": "rust-signal-preprocessor-0.1.0",
"observed": "rust-signal-preprocessor-0.0.9",
"recommendedAction": "sync-runtime-policy"
}
],
"conditions": [
{
"type": "Ready",
"status": "False",
"reason": "RuntimeDrift"
}
]
}
CD repo는 이 contract를 기준으로 environment별 desired runtime policy를 RiskMonitor manifest로 표현합니다. 1단계 source repo 예제는 CRD type, controller, sample manifest를 제공하고, CD repo는 이후 이 manifest를 app-of-apps에 포함할 수 있습니다.
- desired
modelVersion - desired
preprocessingVersion - desired
policyVersion - rollout channel
- site id
Go package는 이 desired state와 current runtime state를 비교해 RiskMonitorStatus를 반환합니다.
manifests/risk-monitor.demo.json은 desired/current input fixture이고, manifests/risk-monitor-status.demo.json은 이 input에 대한 expected output fixture입니다. scripts/validate-contracts.py와 Go app test가 두 파일을 함께 검증합니다.
9. Fixture Contract
9-1. 위치
Fixture는 실제 PHI가 없는 synthetic 또는 public-derived sample만 포함합니다.
fixtures/signals/
README.md
manifest.json
normal-resting.csv
motion-artifact.csv
sensor-dropout.csv
low-spo2-trend.csv
9-2. CSV column
timestamp_ms,sequence_number,ppg_raw,ecg_raw,spo2_permille,heart_rate_bpm,motion_mg,contact_quality
1718600000000,1,512,23,980,72,12,98
1718600000020,2,534,41,980,72,13,98
9-3. Manifest
{
"schemaVersion": "signal.fixture.manifest.v1",
"samplingRateHz": 50,
"scenarios": [
{
"id": "normal-resting",
"file": "normal-resting.csv",
"expectedQuality": "good",
"expectedRiskHint": "low"
},
{
"id": "motion-artifact",
"file": "motion-artifact.csv",
"expectedQuality": "poor",
"expectedRiskHint": "review"
}
]
}
10. API Boundary
10-1. Local HTTP ingestion
HTTP가 1차 local demo의 device ingestion 기본값입니다. MQTT adapter를 추가하더라도 아래 endpoint와 같은 ingestion usecase를 호출해야 합니다.
POST /signal-packets
POST /risk-assessments # test/debug endpoint
GET /monitoring-snapshot
GET /review-queue
GET /review-queue/{assessment_id}
POST /alarms/{alarm_id}/ack
POST /alarms/{alarm_id}/mute
GET /alarms/{alarm_id}/audit-events
GET /healthz
GET /readyz
POST /signal-packets는 signal ingestion usecase를 호출합니다. 이후 MQTT adapter를 추가하더라도 HTTP와 MQTT가 서로 다른 business flow를 만들면 안 됩니다.
POST /risk-assessments는 preprocessed fixture나 직접 입력으로 risk rule을 검증하는 test/debug endpoint입니다. Web dashboard의 기본 데이터 경로는 GET /monitoring-snapshot이며, GET /review-queue는 clinician review backlog와 audit follow-up을 위한 보조 조회 경로입니다.
10-2. Health and readiness
GET /healthz는 process liveness만 표현합니다. Storage dependency가 실패해도 process가 살아 있으면 204를 반환합니다.
GET /readyz는 현재 선택된 storage backend가 요청을 처리할 준비가 되었는지 표현합니다. memory backend는 memory check 하나를 반환하고, postgres-redis backend는 PostgreSQL과 Redis check를 순서대로 반환합니다.
Readiness 실패 응답은 어느 check까지 통과했고 어느 dependency에서 실패했는지 드러내야 합니다.
{
"status": "not-ready",
"serviceName": "patient-risk-python-api",
"storageBackend": "postgres-redis",
"failedCheck": "redis",
"failureType": "TimeoutError",
"checks": [
{ "name": "postgres", "status": "ready", "durationMs": 3 },
{ "name": "redis", "status": "failed", "failureType": "TimeoutError", "durationMs": 1001 }
]
}
durationMs는 dependency별 latency를 비교하기 위한 adapter-level signal입니다. 구체적인 exception message는 response body에 포함하지 않습니다. Credential, host, network detail이 섞일 수 있으므로 adapter는 structured log에만 남기고, external contract에는 check name과 failure type만 노출합니다.
10-3. Error shape
API error는 machine-readable code와 request id를 포함합니다.
{
"error": {
"code": "invalid-signal-packet",
"message": "Signal packet schema validation failed",
"requestId": "req-demo-001"
}
}
11. Validation Responsibility
11-1. 계층별 검증 책임
Contract 검증은 한 곳에 몰아넣지 않고, 각 boundary에서 필요한 만큼 수행합니다.
| Layer | Validation responsibility |
|---|---|
| C/Firmware | sample range, sequence number, device quality flag, packet build |
| MQTT/HTTP ingestion adapter | payload schema, topic/payload scope, duplicate key |
| Rust preprocessing | window size, missingness, feature input validity |
| Python risk usecase | clinical context, model/policy metadata, risk output shape |
| TypeScript dashboard | API response shape, view model mapping, empty/error/stale state |
| Go reconcile core | desired/current runtime policy shape and drift decision |
11-2. Audit context
예제 수준에서도 아래 context는 결과와 log에 남깁니다.
requestIdassessmentIddeviceIddemoSubjectIdmodelVersionpolicyVersionpreprocessingVersionfirmware.version
Raw signal 전체와 PHI는 log에 남기지 않습니다.