update the codebase poc ver1

This commit is contained in:
DatTT127
2026-07-07 15:54:17 +07:00
parent fed5f277f4
commit 1622dc8fc5
452 changed files with 83999 additions and 66328 deletions

View File

@@ -0,0 +1,619 @@
# Screen Analysis: Diagnostic Workspace (WS-25)
> **Document Type:** Design Analysis & Rationale
> **Scope:** Sprint 1-2 FR-25 Synovitis Grading Workspace
> **Directory:** `UI_Screen_Spec/`
> **Status:** Draft
> **Author:** Đạt Trần Tiến (Daves Tran)
> **Date:** June 25, 2026
---
## 1. Analysis Scope
This document analyzes the **Diagnostic Workspace (WS-25)** screen design from multiple perspectives:
- **Structural analysis:** Why this layout, why these zones, why this hierarchy
- **Data architecture analysis:** How state is shaped, how data flows, where it lives
- **Layering analysis:** z-index strategy, canvas stacking, rendering performance
- **NFR constraint analysis:** How each VKIST NFR shaped specific design decisions
- **Comparative analysis:** Why this design over alternatives (modal-heavy, multi-screen, single-panel)
- **Accessibility & safety analysis:** Clinical safety, error prevention, auditability
- **Future-screen analysis:** What comes after Sprint 1-2, how this screen scales
---
## 2. Structural Analysis: Why This Layout?
### 2.1 The Core Tension: Image vs. Information
The fundamental design tension in any clinical imaging workspace is:
> **The clinician needs to see the image (Zone A) AND reason about the result (Zone B) simultaneously.**
If the image and the reasoning interface are on **separate screens** or **toggleable tabs**, the clinician must:
1. Look at the image
2. Memorize or screenshot key features
3. Switch to the reasoning panel
4. Try to reconcile the two contexts mentally
This context-switching is the **exact bottleneck** described in `UC_CONTEXT.md` Step 6 (Manual Multi-Silo Transcription). The whole point of Sprint 1-2 is to eliminate this bottleneck.
**Therefore, the side-by-side layout is non-negotiable.** The DICOM image and the reasoning panel must be co-present.
### 2.2 Why 65/35 Split (Not 50/50 or 70/30)
| Ratio | Problem |
|-------|---------|
| **50/50** | Zone A (DICOM) gets too little space. On a 1920px desktop, Zone A would be 960px — adequate. But on a 1366px laptop (common in hospitals), Zone A shrinks to 683px, making fine-grained annotation difficult. |
| **70/30** | Zone B (Result & Chat) becomes too cramped. The Classification Card, Explanation Panel, Chat Panel, and Override Card all need minimum 350400px to be readable. At 30% on 1366px, Zone B is only 410px — barely workable. |
| **65/35** | Sweet spot. On 1366px: Zone A = 888px (excellent for DICOM), Zone B = 478px (comfortable for reading). On 1920px: Zone A = 1248px (spacious), Zone B = 672px (room for rich content). |
**Mobile breakpoint (stacked 50vh/scroll):** On mobile, the image is given exactly half the viewport because:
- Touch-based annotation requires larger touch targets
- The chat panel needs more vertical space for typing
- 50vh ensures the image is always "big enough to be useful" without dominating the screen
### 2.3 Why Zone B Is Scrollable (Not Fixed Height)
Zone B content varies dramatically across the 6 phases:
- **Phase 1 (Classification):** ~300px of content (card + metrics)
- **Phase 3 (Negotiation):** Can grow to 1000px+ (chat history + drift monitor + input)
- **Phase 4 (Reconfiguration):** ~600px (override card + inspector + summary)
- **Phase 6 (Final Decision):** ~500px (summary + auth + EMR status)
If Zone B had a fixed height with internal scrolling, the chat input would be pinned at the bottom (common pattern). But this creates a problem:
- The **Phase Indicator** at the top would scroll out of view
- The **Quick Actions** at the bottom would compete with the chat input for sticky position
- On mobile, fixed-height Zone B + keyboard open = unusable layout
**Solution:** Zone B scrolls as a whole. The `PhaseIndicator` is sticky within Zone B (not fixed to viewport). This means:
- As the clinician scrolls down through a long chat history, they always see the current phase label
- The chat input is NOT sticky — it sits at the bottom of the chat history, which is natural chat UX
- On mobile, when the keyboard opens, Zone B simply expands to fill available space; the chat input scrolls into view naturally
### 2.4 Why No Modals Over Zone A
The specification explicitly prohibits modals that obscure Zone A. This is a **safety-critical design decision**, not just a UX preference.
**Clinical reasoning:** When a radiologist is looking at an ultrasound image, their visual attention is anchored to specific anatomical features. A modal dialog that covers the image:
- Breaks spatial memory ("where was that suspicious region again?")
- Increases cognitive load (must mentally reconstruct the image from memory)
- Creates risk of misidentification when returning to the image
**Analogy:** This is like a surgeon covering the patient during a critical decision. The image is the patient — it must remain visible.
**Allowed alternatives:**
- Inline confirmation cards within Zone B
- Toast notifications (bottom-center, small, auto-dismiss)
- Inline error banners within Zone B
- RAG-Referee cards injected into chat history
---
## 3. Data Architecture Analysis
### 3.1 Single Store vs. Multiple Stores
The specification proposes a **single Zustand store** (`useDiagnosticWorkspaceStore`) rather than splitting state across multiple stores (e.g., `useDICOMStore`, `useMLStore`, `useChatStore`).
**Rationale:**
- The 6-phase workflow is **tightly coupled**. Phase 4 (Reconfiguration) depends on Phase 1 (Classification) data. Phase 6 (Final Decision) depends on Phase 3 (Negotiation) data.
- Splitting into multiple stores would require **cross-store subscriptions** and **derived selectors** that add complexity without benefit.
- A single store makes **time-travel debugging** easier (one serializable state tree).
- The store size is bounded: max ~20 chat messages, 1 DICOM frame, 1 ML result set. This fits comfortably within the 150MB client memory limit.
**When to split:** If the workspace grows to include multi-session comparison (e.g., showing Month 1 vs Month 3 scans side-by-side), then a `useSessionStore` (per-session data) + `useWorkspaceStore` (UI state) split would be appropriate.
### 3.2 State Shape: Why These Top-Level Keys?
```typescript
interface DiagnosticWorkspaceStore {
sessionId: string | null;
patientId: string | null;
currentPhase: WorkflowPhase;
dicomFrame: ImageBitmap | null;
dicomMetadata: {...} | null;
mlResults: {...} | null;
clinicianGrade: number | null;
isOverriding: boolean;
overrideJustification: string;
markAsGroundTruth: boolean;
maskedRegions: MaskedRegion[];
chatMessages: ChatMessage[];
driftScore: number | null;
refereeIntervened: boolean;
zoomLevel: number;
panOffset: { x: number; y: number };
activeTool: 'pan' | 'zoom' | 'brush' | 'eraser' | 'caliper' | 'pin';
showGradCAM: boolean;
showSegmentation: boolean;
sidebarCollapsed: boolean;
// ... actions
}
```
**Key observations:**
1. **`dicomFrame` is `ImageBitmap | null`, not `string` (URL)**
- ImageBitmap is GPU-friendly (transferred to GPU memory once, not re-decoded on every render)
- Using a URL would require re-decoding the PNG on each pan/zoom, causing jank
- The DICOM → PNG conversion happens once in a Web Worker; the resulting ImageBitmap is shared with the canvas layers
2. **`mlResults` is a single object, not separate stores for each model output**
- The ML pipeline is atomic: angle → inflammation → segmentation → grade are computed in sequence
- Splitting them would create intermediate states (e.g., `angleClassification` exists but `segmentationMask` is null) that the UI must handle
- A single `mlResults` object ensures the UI only renders when the **full pipeline** is complete
3. **`maskedRegions` is an array of SVG path data, not raster masks**
- SVG paths are resolution-independent (critical for zoom/pan)
- They are also lightweight (a few KB per stroke) compared to raster masks (MBs)
- The actual pixel mask is computed server-side from the SVG paths when saving (`UC-62864`)
4. **`chatMessages` is bounded by implementation, not by the type**
- The spec says "pruned to last 20 messages" — this is an **implementation detail**, not enforced by TypeScript
- Older messages are compressed to `ChatMessageSummary` objects (user message + timestamp + first 50 chars of LLM reply) and stored in IndexedDB, not kept in React state
5. **`zoomLevel` and `panOffset` are top-level, not nested in `uiState`**
- These are updated on every `requestAnimationFrame` during pan/zoom
- Keeping them at the top level minimizes selector overhead (no nested property access)
- They are **not persisted** to IndexedDB (session-only state)
### 3.3 Data Flow: Optimistic UI vs. Server-Authoritative
| Data | Source | UI Strategy | Reason |
|------|--------|-------------|--------|
| DICOM frame | Client (upload) or PACS | Optimistic render (show immediately, validate in background) | DICOM files can be 50200MB; waiting for server validation would block the UI for seconds |
| ML results | Server (Triton) | Server-authoritative (show only after SSE completion) | ML inference is heavy; showing partial results would be misleading |
| Grade selection | Client | Optimistic (update UI immediately, debounce save to server) | Grade changes happen in rapid succession (clinician trying different values); saving each change would spam the API |
| Chat messages | Client (user) + Server (LLM) | Hybrid: user messages optimistic, LLM messages server-authoritative | User sees their message instantly; LLM messages arrive via SSE stream |
| Masked regions | Client | Optimistic (render immediately, save on commit) | Brush strokes must feel instantaneous; saving each stroke would cause lag |
**Optimistic update pattern for grade selection:**
```typescript
// When clinician changes grade:
setGrade(newGrade); // Immediate UI update
debounce(() => saveGrade(newGrade), 500); // API call after 500ms of inactivity
```
---
## 4. Layering Analysis
### 4.1 Why Multi-Layer Canvas (Not Single Canvas)
Zone A uses **5 separate canvas/SVG layers**, not a single canvas redrawn on every interaction.
**Single canvas approach problems:**
- Every pan/zoom requires re-rendering: background + GradCAM + segmentation + annotations + HUD
- The DICOM background image is large (often 1024×768 or larger); re-drawing it on every frame is expensive
- GradCAM and segmentation are semi-transparent PNGs with alpha channels; compositing them with the background requires `globalAlpha` manipulation that is hard to optimize in a single canvas
**Multi-layer approach benefits:**
- `CanvasLayer_Background` is rendered **once** (on session load) and never redrawn during pan/zoom
- `CanvasLayer_GradCAM` and `CanvasLayer_Segmentation` are rendered **once** (on ML completion) and only redrawn on zoom/pan (via CSS transform on the container, not canvas re-render)
- `CanvasLayer_Annotations` is the only layer redrawn on every brush stroke — and it's an SVG layer, not a canvas, so it's GPU-accelerated via CSS transforms
- `CanvasLayer_ViewportInfo` (HUD) is updated on `mousemove` but contains only text — redrawing text on canvas is cheap
**Performance implication:**
- Pan/zoom interaction: only `CanvasLayer_ViewportInfo` redraws (text), all other layers move via CSS `transform: translate()`**zero canvas redraws during pan/zoom**
- Brush stroke: only `CanvasLayer_Annotations` redraws (SVG path append) — **1 lightweight redraw**
- GradCAM opacity change: only `CanvasLayer_GradCAM` redraws (changes CSS `opacity` property, not canvas content) — **zero canvas redraws**
### 4.2 z-index Strategy: Why These Specific Values
The z-index map uses **gaps** (0, 10, 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000) rather than sequential values (1, 2, 3...).
**Reason:**
- Gaps allow **future insertion** without refactoring. If we need a new layer between segmentation (z:200) and annotations (z:300), we can use z:250 without changing existing values.
- The specific values encode **semantic groups**:
- **099:** Canvas layers (Zone A content)
- **100399:** Zone A UI elements (toolbar, HUD)
- **400499:** Boundary between Zone A and Zone B
- **500799:** Zone B internal elements (phase indicator, chat input, quick actions)
- **800899:** Zone B overlays (RAG-Referee card, final decision CTA)
- **900999:** System chrome (top bar, bottom bar, toasts)
**Critical rule:** No Zone B element may exceed z-index 799 **unless** it is a system chrome element (top/bottom bar) or a transient overlay (toast, tooltip). This ensures Zone A's canvas is never visually obscured by Zone B content.
### 4.3 SVG Annotations vs. Canvas Annotations
The specification uses **SVG for brush strokes and calipers** rather than drawing them on a canvas layer.
**Why SVG:**
- SVG paths are **resolution-independent** — they scale perfectly when the clinician zooms in/out
- SVG elements are **DOM elements** — they can have event listeners (click, hover) for interaction
- SVG paths can be **serialized** easily (for saving to IndexedDB or sending to the backend)
- SVG supports **boolean operations** (union, intersection, difference) for complex mask shapes
**Why not canvas:**
- Canvas drawings are raster — they pixelate when zoomed in
- Canvas requires manual hit-testing for interaction
- Canvas drawings must be manually serialized to PNG/SVG for storage
---
## 5. NFR Constraint Analysis: How NFRs Shaped the Design
### 5.1 NFR-1: Collaborative Rendering Speed ≤ 3.0s
**Design implications:**
- **No client-side ML rendering.** GradCAM and segmentation are pre-rendered as PNGs on the server (Triton + Python PIL). The client only composites them onto the canvas.
- **Streaming SSE for ML progress.** The UI shows incremental progress (angle → inflammation → segmentation → grade) so the clinician sees activity within 12 seconds, even if full pipeline takes 3s.
- **ImageBitmap for DICOM frames.** ImageBitmap is transferred to GPU memory once and reused; no re-decoding on every frame.
- **Zone B renders progressively.** Classification Card appears first; Explanation Panel and Chat Panel load lazily.
**What would happen without these optimizations:**
- Client-side GradCAM rendering (WebLLM generating heatmaps in browser) would take 1030 seconds
- Single-canvas approach with full redraw on every pan would cause visible jank
- Non-streamed API calls would show a blank screen for 3+ seconds
### 5.2 NFR-4: Client Memory Footprint ≤ 150 MB
**Design implications:**
- **Chat history pruning.** Only 20 messages kept in React state. Older messages are serialized to `ChatMessageSummary` (text only, no ImageBitmaps) and stored in IndexedDB.
- **GradCAM sprite sheets.** Instead of storing full-resolution heatmaps (1024×768 × 4 bytes = 3MB each), GradCAM is rendered as a tiled sprite sheet at reduced resolution (512×384 = 0.75MB).
- **DICOM frame release on session lock.** After signing, `dicomFrame` and `mlResults` are set to `null` and the ImageBitmap is explicitly closed (`imageBitmap.close()`) to free GPU memory.
- **SVG annotations, not raster masks.** SVG paths are a few KB each; raster masks would be MBs.
**Memory budget breakdown (target):**
| Component | Estimated Memory |
|-----------|-----------------|
| DICOM frame (ImageBitmap) | 36 MB |
| GradCAM heatmap (ImageBitmap) | 0.751.5 MB |
| Segmentation mask (ImageBitmap) | 0.751.5 MB |
| Chat history (20 messages) | < 0.5 MB |
| Zustand store (JS objects) | < 5 MB |
| React component tree | 2040 MB |
| Browser overhead | 5080 MB |
| **Total** | **80135 MB** (within 150 MB limit) |
### 5.3 NFR-14: Legacy Hardware Compatibility
**Design implications:**
- **CSS-only animations.** No GSAP, no Framer Motion. All animations use CSS `transition` or `@keyframes`.
- **Canvas fallback to CPUSpriteAdapter.** If WebGL is unavailable (old integrated GPUs), the canvas falls back to CPU rendering via `CPUSpriteAdapter` (per frontend spec). This is slower but functional.
- **System font stack.** No Google Fonts or Adobe Fonts. Fonts are loaded from the local system (`-apple-system`, `Segoe UI`, `Roboto`, etc.).
- **No WebGPU, no WebAssembly for ML.** Edge ML uses LiteRT (TensorFlow Lite Runtime) which has a pure JavaScript fallback for devices without WASM support.
**What this rules out:**
- 3D transforms on the DICOM canvas (would require WebGL)
- Real-time video-style rendering of GradCAM (would require GPU compute)
- Heavy animation libraries (Framer Motion, Lottie)
### 5.4 NFR-16: Air-Gapped Infrastructure
**Design implications:**
- **No external CDN.** All assets (JS bundles, CSS, fonts, images) are served from the local NGINX reverse proxy.
- **LLM Explainer runs locally.** GemmaE2B runs via WebLLM (in-browser) OR via a local FastAPI endpoint. No calls to external LLM APIs (OpenAI, Google, etc.).
- **RAG-Referee uses local pgvector.** The knowledge base (OMERACT guidelines) is stored in local PostgreSQL with pgvector extension. No external vector DB.
- **No external analytics.** No Google Analytics, no Mixpanel, no Sentry (unless self-hosted).
**Operational impact:**
- The PWA cannot be loaded from a phone on cellular data it must be on the hospital Wi-Fi
- If the hospital LAN goes down, the PWA shows "Offline" and disables features that require the backend
- The "Cloud Fallback" (NFR-16a) is an explicit, logged exception not a silent failover
### 5.5 Decree 13: Data Privacy
**Design implications:**
- **Chat contains no PHI.** Chat messages reference clinical abbreviations ("Grade 2", "Doppler 42%", coordinates) but never patient names, IDs, or dates of birth.
- **DICOM scrubbing before render.** The DICOM parser (`pydicom` in Web Worker) strips all PHI tags (PatientName, PatientID, DOB, etc.) before the image is rendered to canvas.
- **Encrypted IndexedDB.** All session data stored locally is encrypted via WebCrypto (AES-256-GCM) before writing to IndexedDB.
- **Audit trail in chat CoT.** The full chat transcript is included in the EMR JSON payload, providing a tamper-evident record of the clinical reasoning process.
---
## 6. Comparative Analysis: Why Not Alternative Layouts?
### 6.1 Alternative 1: Full-Screen DICOM + Floating Result Panel
```
┌─────────────────────────────────────┐
│ │
│ DICOM Canvas │
│ (full screen) │
│ │
│ ┌───────────────────────────┐ │
│ │ Grade: 2 | Confidence: 78%│ │ ← floating card
│ └───────────────────────────┘ │
│ │
│ ┌───────────────────────────┐ │
│ │ 💬 Chat (expandable) │ │ ← hidden by default
│ └───────────────────────────┘ │
└─────────────────────────────────────┘
```
**Pros:**
- Maximum image real estate
- Minimalist aesthetic
**Cons:**
- Chat panel is **hidden by default** clinician must actively open it
- When chat is open, it covers 3040% of the image
- Classification card floats over the image, potentially obscuring the region being discussed
- **Verdict: Rejected.** The chat and explanation are primary clinical tools, not secondary utilities. They must be co-present, not hidden.
### 6.2 Alternative 2: Three-Panel Layout (DICOM | Metrics | Chat)
```
┌──────────┬──────────┬──────────┐
│ │ │ │
│ DICOM │ Metrics │ Chat │
│ Canvas │ Panel │ Panel │
│ (50%) │ (25%) │ (25%) │
│ │ │ │
└──────────┴──────────┴──────────┘
```
**Pros:**
- Clean separation of concerns
- Each panel has a dedicated purpose
**Cons:**
- Metrics Panel is mostly empty during Phase 3 (Negotiation) wasted space
- Chat Panel is too narrow for comfortable reading (25% of 1366px = 341px)
- The Metrics data (synovial thickness, Doppler %) is already displayed in the Classification Card within the Chat Panel **duplication**
- **Verdict: Rejected.** The Metrics Panel is redundant; its content belongs in the Classification Card. The three-panel layout wastes space during non-metric phases.
### 6.3 Alternative 3: Tabbed Interface (Image | Results | Chat)
```
┌─────────────────────────────────────┐
│ [Image] [Results] [Chat] │ ← tabs
├─────────────────────────────────────┤
│ │
│ │
│ Active tab content │
│ │
│ │
└─────────────────────────────────────┘
```
**Pros:**
- Maximum space for each view
- Simple mental model
**Cons:**
- **Tab switching is context-switching** the exact bottleneck we're trying to eliminate
- The clinician cannot see the image while reading the explanation or typing in chat
- Violates the core principle: "The DICOM image is the north star"
- **Verdict: Rejected.** Tab switching reintroduces the manual multi-silo transcription problem.
### 6.4 Alternative 4: Single-Screen with Collapsible Sidebar
```
┌─────────────────────────────────────┐
│ DICOM Canvas │
│ (full width) │
│ │
│ ┌───────────────────────────┐ │
│ │ Sidebar (collapsible) │ │
│ │ - Classification │ │
│ │ - Explanation │ │
│ │ - Chat │ │
│ └───────────────────────────┘ │
└─────────────────────────────────────┘
```
**Pros:**
- Full-screen image when sidebar is collapsed
- All reasoning tools in one place
**Cons:**
- Sidebar collapse/expand is a **manual action** that adds friction
- When sidebar is open, the image is squeezed to ~60% width same as our design but with extra UI chrome (collapse button, resize handle)
- The sidebar would need internal scrolling, creating the same Zone B scroll behavior but with more complexity
- **Verdict: Rejected.** Our design achieves the same layout without the collapse/expand friction. The Zone B width is always available; no manual resizing required.
---
## 7. Accessibility & Clinical Safety Analysis
### 7.1 Color Blindness
**Problem:** The grade color coding (green=0, amber=1, orange=2, red=3) relies on color distinction. Approximately 8% of males have some form of color blindness.
**Mitigation:**
- Grade pills include **text labels** ("Grade 2") not just color
- Grade pills have **distinct shapes/icons** in addition to color (planned for Sprint 2)
- The confidence bar uses both color AND percentage text
- Critical status (low confidence, circuit breaker) uses **icons** (⚠) in addition to color
**Current gap (Sprint 1-2):** No shape differentiation for grades. This is a SHOULD HAVE for Sprint 2.
### 7.2 Touch Target Size
**Problem:** Hospital clinicians may use tablets or touch screens. Touch targets must be at least 44×44px (WCAG 2.1 / Apple HIG).
**Current compliance:**
- All buttons in the canvas toolbar: 44×44px
- Grade selector dropdown: 44px height
- Chat input: 44px height
- Sign button: 48px height
- Back button: 44×44px
**Risk areas:**
- Quick chips in chat ("Artifact", "Chronic", "Equipment"): currently 32px height **below WCAG minimum**. Should be increased to 3640px minimum.
- RAG-Referee card links: text links may be too small for touch. Should be converted to button-style links.
### 7.3 Error Prevention (Clinical Safety)
| Error Scenario | Prevention Mechanism |
|----------------|---------------------|
| Wrong patient selected | Top Bar shows patient ID + scan type prominently; back button is large and always visible |
| Wrong grade selected | Grade dropdown pre-populated with AI suggestion; non-adjacent grade changes require secondary confirmation |
| Signing without review | Sign button disabled until confidence 60% and all validations pass; Phase Indicator shows current phase |
| Accidental navigation away | `beforeunload` event prompts if session is not signed or saved |
| Misinterpretation of GradCAM | GradCAM thumbnail in Explanation Panel has label: "Key visual evidence tap to highlight"; tooltip explains heatmap = "regions that influenced the classification" |
| LLM hallucination | BERT drift monitor + RAG-Referee auto-intervention; chat input briefly locked when drift > 0.7 |
### 7.4 Audit Trail
Every clinical decision in this workspace generates an audit log entry:
| Action | Audit Data |
|--------|-----------|
| Grade changed | Timestamp, old grade, new grade, user ID |
| Mask drawn | SVG path data, timestamp, area %, user ID |
| Chat message sent | Message text, timestamp, user ID, pinned coordinate (if any) |
| RAG-Referee intervention | Trigger reason, retrieved guideline, alignment score, timestamp |
| Session signed | Cryptographic hash of all above data, user ID, timestamp, EMR txHash |
This audit trail is:
- **Immutable** (append-only PostgreSQL table with no UPDATE/DELETE triggers)
- **Complete** (every interaction is logged, not just final decisions)
- **Traceable** (chat CoT allows reconstructing the exact reasoning path)
---
## 8. Future Screen Analysis
### 8.1 What Happens After Sprint 1-2?
The Sprint 1-2 scope is **FR-25 only** (Synovitis Grading for knee ultrasound). The architecture is designed to scale to additional FRS:
| Future FR | New Screen / Extension | Relationship to WS-25 |
|-----------|------------------------|------------------------|
| **FR-26** (View Patient List) | Patient List Screen (`UC-48377`) | Entry point to WS-25 |
| **FR-16/31/12** (Patient Portal / Journey Mirror) | Patient Timeline Screen | Consumer of finalized WS-25 data |
| **FR-28/29/30** (Symptom Journal) | Journal Screen | Parallel to WS-25, same patient |
| **FR-25 extension: Shoulder/Hip** | Same WS-25 with different anatomy atlas | Zone A changes anatomy labels; Zone B changes metric thresholds |
| **FR-25 extension: PT View** | Therapist Workspace (read-only WS-25 + prescription) | Zone B becomes read-only; Zone A shows same scan with PT-specific overlays |
### 8.2 WS-25 as a Design Pattern
The Diagnostic Workspace is not just a single screen — it is a **design pattern** that can be replicated for other diagnostic tasks:
**Pattern: "Diagnostic Workspace"**
```
┌─────────────────────────────────────────┐
│ TOP BAR (context + actions) │
├───────────────────┬─────────────────────┤
│ │ │
│ ZONE A │ ZONE B │
│ (Primary Artifact│ (Reasoning + Chat) │
│ Viewport) │ │
│ │ │
├───────────────────┴─────────────────────┤
│ BOTTOM BAR (status + NFR) │
└─────────────────────────────────────────┘
```
**Applicable to:**
- Radiology (X-ray, CT, MRI interpretation)
- Pathology (slide scanning)
- Dermatology (skin lesion imaging)
- Ophthalmology (retinal imaging)
**Pattern invariants:**
1. Zone A must always show the primary artifact (image, slide, scan)
2. Zone B must always contain the AI suggestion + human override
3. Chat/reasoning panel is part of Zone B, not a separate screen
4. Top Bar contains identity + sign/finalize action
5. Bottom Bar contains operational status (NFR compliance)
### 8.3 Scaling Zone A for 3D / Multi-Slice
Currently, Zone A renders a **single 2D ultrasound frame**. Future FRS may require:
- **Multi-slice scroll:** Stack of frames (e.g., 20 frames from a sweep)
- **3D volume rendering:** Reconstructed volume from swept ultrasound
- **Comparison mode:** Side-by-side of Month 1 vs Month 3 scans
**Design implications for Zone A:**
- The `CanvasLayer_*` stack must support **multiple frame indices**
- A `FrameScrubber` component (timeline scrubber at the bottom of Zone A) would allow frame-by-frame navigation
- Comparison mode would require **two `ZoneA_DICOMCanvas` instances** side-by-side, each with independent zoom/pan state
- The current multi-layer canvas approach scales naturally to these extensions — we just add/remove layers, not redesign the architecture
### 8.4 Scaling Zone B for Multi-Stakeholder
Currently, Zone B is designed for **one actor (UP5)**. Future FRS may require:
- **Surgeon view (UP7):** Same scan, but Zone B shows treatment planning tools (3D anatomy overlay, surgical approach markers)
- **Patient view (UP8):** Simplified Zone B — no GradCAM, no chat, just plain-language explanation + progress chart
- **PT view (UP6):** Zone B shows kinetic overlay + exercise prescription derived from the scan
**Design implications for Zone B:**
- Zone B content should be **role-driven** — a `RoleConfig` determines which sections are visible
- The 6-phase state machine may differ by role (e.g., UP8 never enters "Negotiation" phase)
- Chat participants differ by role (UP7 chats with UP5's report, not with the LLM directly)
---
## 9. Risk Analysis & Mitigations
### 9.1 Design Risks
| Risk | Likelihood | Impact | Mitigation |
|------|-----------|--------|------------|
| Zone B too narrow on 1366px screens | Medium | Medium | Test on 1366px; if Zone B < 420px, switch to collapsible sidebar pattern |
| Chat input hidden behind mobile keyboard | Medium | High | On mobile, when keyboard opens, Zone A shrinks to 30vh and Zone B expands; test on iOS Safari + Android Chrome |
| GradCAM opacity makes scan unreadable | Low | Medium | Default opacity 0.4; clinician can adjust via slider (Sprint 2); emergency toggle to hide all overlays |
| Phase transitions confusing | Medium | Medium | Phase indicator is always visible; phase labels are plain language ("Review Grade" not "Phase 1: Auto-Classification"); tooltip explains current phase on first load |
| Circuit Breaker fires unexpectedly | Low | High | BERT drift threshold is tunable; default 0.7 is conservative; circuit breaker can be manually overridden by senior clinician |
### 9.2 Implementation Risks
| Risk | Likelihood | Impact | Mitigation |
|------|-----------|--------|------------|
| Zustand store becomes "god object" | Medium | Medium | Enforce strict separation: store holds state only, no business logic; all logic in custom hooks (`useMLPipeline`, `useChat`, `usePhaseManager`) |
| Canvas layer desync (GradCAM doesn't match segmentation) | Low | High | All layers share a single `sessionId` + `frameTimestamp`; on any layer update, verify timestamp match; if mismatch, re-render all layers |
| Chat state lost on browser refresh | Medium | Medium | Chat state persisted to IndexedDB (encrypted) on every message; on session load, restore from IndexedDB before fetching from server |
| ImageBitmap memory leak | Low | High | Explicit `imageBitmap.close()` on session lock and on component unmount; use React `useEffect` cleanup |
---
## 10. Open Questions & Unresolved Design Issues
### 10.1 Open Questions (Requiring Stakeholder Input)
| # | Question | Impact | Owner |
|---|----------|--------|-------|
| 1 | Should the Phase Indicator allow jumping to any phase, or only to previously visited phases? | Determines whether clinician can skip ahead to "Final Decision" without going through explanation/chat | UP5 + UP1 (Senior Expert) |
| 2 | What is the maximum acceptable chat history length? (20 messages is an engineering estimate) | Affects memory usage and IndexedDB schema | UP5 + Engineering |
| 3 | Should the Ground-Truth Override toggle default to ON, or require explicit opt-in? | Default ON means more retraining data but may concern clinicians who don't want their corrections used for training | UP5 + Ethics Committee |
| 4 | In Phase 4, should the Pixel Activation Inspector show original vs. corrected side-by-side, or as an overlay diff? | Affects how clinicians perceive the impact of their masking | UP5 + Design Agent |
| 5 | Should the Sign button require biometric re-authentication every time, or only when JWT is expiring? | Affects workflow friction vs. security | UP1 + Security Team |
### 10.2 Unresolved Design Issues (Engineering Decisions)
| # | Issue | Options | Recommended |
|---|-------|---------|-------------|
| 1 | Chat input position: sticky bottom of Zone B vs. sticky bottom of viewport | A) Sticky within Zone B (scrolls with content) B) Fixed to viewport bottom (always visible) | **B** Chat input should always be visible, even when scrolling through long history. However, this conflicts with the "no fixed elements within Zone B" rule. Compromise: Chat input is sticky within Zone B, but Zone B has a max-height with its own scroll container when chat is active. |
| 2 | GradCAM thumbnail in Explanation Panel: static image vs. animated loop | A) Static PNG B) 2-second looping GIF of heatmap fading in/out | **A** GIF increases memory and bundle size. Static is sufficient; the full GradCAM is already visible in Zone A. |
| 3 | Drift Monitor placement: inside chat panel vs. separate Zone B section | A) Inside ChatInputBar (compact) B) Separate section above chat history | **A** Compact placement reduces Zone B height. Drift score is secondary information; the primary signal is the RAG-Referee intervention card. |
| 4 | Override justification: free text vs. structured form | A) Free textarea (current design) B) Structured form (dropdown for reason + optional text) | **A** for Sprint 1-2. Structured form may be better for analytics but adds friction. Free text allows clinicians to express nuance. |
---
## 11. Design Principles Derived from This Analysis
These principles should guide all future screen designs in the VKIST MSK project:
### 11.1 The North Star Principle
> **The primary clinical artifact (image, scan, record) must never be obscured by reasoning tools.**
If a reasoning tool (chat, explanation, metrics) would cover the artifact, resize or reposition the artifact never the reasoning tool.
### 11.2 The Zero-Context-Switch Principle
> **All reasoning about a clinical artifact must happen in the same viewport as the artifact.**
If the clinician must click, tap, or navigate to reason about what they see, the design has failed.
### 11.3 The Safety-First Layering Principle
> **Critical safety information (confidence, circuit breaker, drift) must be visible without scrolling or interaction.**
The confidence bar, phase indicator, and drift monitor are always in the viewport without scrolling. Explanations and chat history can scroll they are not safety-critical.
### 11.4 The Air-Gap Transparency Principle
> **The system must always communicate its infrastructure state (online/offline, cloud fallback, sync status) without the clinician having to ask.**
The Bottom Bar, Network Indicator, and EMR Status are always visible. The clinician never wonders "is the system working?"
### 11.5 The Audit-by-Design Principle
> **Every clinical interaction must produce an immutable, serializable trace without extra work from the clinician.**
The chat CoT, override justification, masked regions, and BERT drift scores are all automatically captured. The clinician does not need to "enable logging" or "save notes" the audit trail is a byproduct of normal use.
---
*End of Screen Analysis: Diagnostic Workspace (WS-25)*

View File

@@ -0,0 +1,68 @@
# View Patient List with Clinical Summary
Actor: Diagnostic Radiologist (Rad)
DateAdd: June 25, 2026 7:20 PM
Engineer: Đạt Trần Tiến (Daves Tran)
Functional Requirement Engineer DB: VIEW PATIENT LIST WITH RESPONSIBILITY DETAILS
Goal: Access a structured list of patients under the user's clinical responsibility to monitor active diagnostic cases and treatment tracks.
Interaction: User-to-System
Stimulus: The user clicks the "View Patient List" button in the Mobile Web PWA dashboard.
SysResponse: The system retrieves the authenticated user's patient roster and displays a scrollable list, each entry showing patient anonymized ID, most recent scan type, current diagnostic status (e.g., AI-graded, pending review, finalized), prescription summary, and active workflow stage.
Title [Verb + Noun]: View Patient List with Clinical Summary
UC-ID: UC-48377
VerboseForm: The use case 'View Patient List with Clinical Summary' defines a User-to-System interaction where the Diagnostic Radiologist (Rad) aims to Access a structured list of patients under the user's clinical responsibility to monitor active diagnostic cases and treatment tracks. This workflow is triggered when The user clicks the "View Patient List" button in the Mobile Web PWA dashboard, causing the system to respond by providing The system retrieves the authenticated user's patient roster and displays a scrollable list, each entry showing patient anonymized ID, most recent scan type, current diagnostic status (e.g., AI-graded, pending review, finalized), prescription summary, and active workflow stage.
```markdown
```markdown
# Use Case Deep-Dive: View Patient List with Clinical Summary
## 1. Structural Preconditions & Postconditions
* **Preconditions:**
* User has valid authentication credentials and active session with the Mobile Web PWA.
* User has at least one patient case assigned under their clinical responsibility scope.
* **Postconditions (Success State):**
* Patient roster list is fully rendered in the mobile viewport.
* Each list entry contains anonymized patient ID, scan type, diagnostic status, prescription summary, and workflow stage.
---
## 2. Interaction Scenarios (Step-by-Step Flow)
### Main Success Scenario (Happy Path)
1. **Diagnostic Radiologist** taps the "My Patients" navigation chip on the PWA dashboard.
2. **System** validates the active JWT token and queries the backend patient roster endpoint.
3. **System** retrieves the authenticated user's assigned patient list with associated clinical metadata (scan type, AI grade status, prescription summary, workflow stage).
4. **System** renders a scrollable, paginated list on the mobile viewport, with each card showing anonymized patient ID, most recent scan modality, current diagnostic status tag, and active treatment workflow indicator.
5. **System** enables pull-to-refresh to reload the roster without full page reload.
### Alternative & Exception Flows
* **Exception Flow A: Empty Patient Roster**
* At step [3], if the authenticated user has no assigned patients, the system displays a placeholder state with a "No patients assigned" message and a contact administrator prompt.
* **Exception Flow B: Network Timeout / Session Expiry**
* At step [2], if the backend request times out or the session token is invalid, the system displays a non-blocking connection error banner with a retry action.
---
## 3. PlantUML Visual Model
```plantuml
@startuml
left to right direction
skin rose
actor "Diagnostic Radiologist" as Rad
rectangle "VKIST MSK Workspace (FR-26)" {
usecase "View Patient List with Clinical Summary" as UC_Core
usecase "UC-48378\nFilter Patient List by Clinical Status" as UC_Filter
usecase "UC-48379\nNavigate to Patient Detail Record" as UC_Detail
}
Rad --> UC_Core
UC_Core ..> UC_Filter : <<extend>>
UC_Core ..> UC_Detail : <<include>>
@enduml
```
---

View File

@@ -0,0 +1,66 @@
# Filter Patient List by Clinical Status
Actor: Diagnostic Radiologist (Rad)
DateAdd: June 25, 2026 7:20 PM
Engineer: Đạt Trần Tiến (Daves Tran)
Functional Requirement Engineer DB: VIEW PATIENT LIST WITH RESPONSIBILITY DETAILS
Goal: Narrow the patient roster to a specific diagnostic or treatment sub-population to reduce cognitive load during high-volume clinical sessions.
Interaction: User-to-System
Stimulus: The user applies a filter (by status, scan type, or date range) to the currently displayed patient list.
SysResponse: The system immediately updates the list to show only patients matching the selected criteria, with visible filter chips indicating active constraints.
Title [Verb + Noun]: Filter Patient List by Clinical Status
UC-ID: UC-48378
VerboseForm: The use case 'Filter Patient List by Clinical Status' defines a User-to-System interaction where the Diagnostic Radiologist (Rad) aims to Narrow the patient roster to a specific diagnostic or treatment sub-population to reduce cognitive load during high-volume clinical sessions. This workflow is triggered when The user applies a filter (by status, scan type, or date range) to the currently displayed patient list, causing the system to respond by providing The system immediately updates the list to show only patients matching the selected criteria, with visible filter chips indicating active constraints.
```markdown
```markdown
# Use Case Deep-Dive: Filter Patient List by Clinical Status
## 1. Structural Preconditions & Postconditions
* **Preconditions:**
* Full patient roster is loaded and displayed in the PWA viewport (`UC-48377`).
* At least one filter dimension is available (status, scan type, date range).
* **Postconditions (Success State):**
* List viewport is updated to show only matching patient entries.
* Active filter chips are rendered to indicate current constraint state.
---
## 2. Interaction Scenarios (Step-by-Step Flow)
### Main Success Scenario (Happy Path)
1. **Diagnostic Radiologist** taps a filter chip or dropdown control above the patient list (e.g., "Pending Review", "AI-Graded", "Sup-Long", "Last 7 Days").
2. **System** captures the selected filter parameters and re-queries the patient roster endpoint with applied constraints.
3. **System** updates the rendered list in-place, preserving the scroll position and removing non-matching entries.
4. **System** displays active filter chips above the list to provide clear visual feedback of current constraints.
5. **System** enables the user to stack additional filters or clear all filters with a single "Reset" action.
### Alternative & Exception Flows
* **Exception Flow A: No Results After Filter**
* At step [3], if the applied filter yields zero matching patients, the system displays a "No patients match current filters" empty state with a direct "Clear Filters" action button.
* **Exception Flow B: Partial Filter Failure**
* At step [2], if one of multiple selected filters fails to apply (e.g., server timeout on a date-range constraint), the system retains the last successfully filtered list state and highlights the failed filter chip in an error state.
---
## 3. PlantUML Visual Model
```plantuml
@startuml
left to right direction
skin rose
actor "Diagnostic Radiologist" as Rad
rectangle "VKIST MSK Workspace (FR-26)" {
usecase "View Patient List with Clinical Summary" as UC_List
usecase "Filter Patient List by Clinical Status" as UC_Core
}
Rad --> UC_Core
UC_List <.. UC_Core : <<extend>> (Applied to active list)
@enduml
```
---

View File

@@ -0,0 +1,69 @@
# Navigate to Patient Detail Record
Actor: Diagnostic Radiologist (Rad)
DateAdd: June 25, 2026 7:20 PM
Engineer: Đạt Trần Tiến (Daves Tran)
Functional Requirement Engineer DB: VIEW PATIENT LIST WITH RESPONSIBILITY DETAILS
Goal: Drill into a specific patient's consolidated clinical workspace to review full diagnostic history, AI-generated overlays, and treatment context before rendering a final judgment.
Interaction: User-to-System
Stimulus: The user taps a specific patient entry from the list (filtered or unfiltered).
SysResponse: The system navigates to the patient's full clinical workspace, displaying longitudinal diagnostic history, AI-segmented anatomical overlays with GradCAM heatmaps, prescription and diagnosis summaries, and current treatment protocol notes.
Title [Verb + Noun]: Navigate to Patient Detail Record
UC-ID: UC-48379
VerboseForm: The use case 'Navigate to Patient Detail Record' defines a User-to-System interaction where the Diagnostic Radiologist (Rad) aims to Drill into a specific patient's consolidated clinical workspace to review full diagnostic history, AI-generated overlays, and treatment context before rendering a final judgment. This workflow is triggered when The user taps a specific patient entry from the list (filtered or unfiltered), causing the system to respond by providing The system navigates to the patient's full clinical workspace, displaying longitudinal diagnostic history, AI-segmented anatomical overlays with GradCAM heatmaps, prescription and diagnosis summaries, and current treatment protocol notes.
# Use Case Deep-Dive: Navigate to Patient Detail Record
## 1. Structural Preconditions & Postconditions
* **Preconditions:**
* Patient roster list is rendered in the PWA viewport (`UC-48377`).
* Patient record contains at least one diagnostic session entry accessible by the authenticated user.
* **Postconditions (Success State):**
* Full patient workspace is loaded, showing consolidated diagnostic history, AI overlays, prescription summaries, and active treatment protocol notes.
* All displayed data is Decree 13-scrubbed (no raw PII visible to the user).
---
## 2. Interaction Scenarios (Step-by-Step Flow)
### Main Success Scenario (Happy Path)
1. **Diagnostic Radiologist** taps a patient card entry from the displayed roster list.
2. **System** sends a detail-record fetch request to the backend with the patient session identifier.
3. **System** retrieves longitudinal diagnostic history, including prior scan metadata, AI-generated segmentation overlays, GradCAM heatmaps, prescription summaries, and active treatment protocol notes.
4. **System** validates that all data has been through the Decree 13 scrubbing layer before rendering.
5. **System** renders the consolidated patient workspace, displaying a timeline of prior scans, the most recent AI-graded overlays, and current prescription and diagnosis summaries in a single rapid-consumption view.
6. **System** enables the radiologist to tap any historical scan entry to load the full diagnostic workspace (`UC-48376`).
### Alternative & Exception Flows
* **Exception Flow A: Patient Record Locked or Restricted**
* At step [2], if the patient record is flagged with a access restriction (e.g., different responsible clinician), the system displays an "Access Restricted" dialog with a request access action rather than exposing partial data.
* **Exception Flow B: Incomplete Scrubbing Detected**
* At step [4], if the scrubbing validation layer detects residual PII tokens in the payload, the system halts rendering and triggers a server-side re-scrubbing cycle before presenting the record.
---
## 3. PlantUML Visual Model
```plantuml
@startuml
left to right direction
skin rose
actor "Diagnostic Radiologist" as Rad
rectangle "VKIST MSK Workspace (FR-26)" {
usecase "View Patient List with Clinical Summary" as UC_List
usecase "Filter Patient List by Clinical Status" as UC_Filter
usecase "Navigate to Patient Detail Record" as UC_Core
usecase "UC-48376\nLoad Patient Scan Session" as UC_Scan
}
Rad --> UC_Core
UC_List ..> UC_Core : <<include>>
UC_Filter <.. UC_Core : (From filtered list)
UC_Core ..> UC_Scan : <<include>>
@enduml
```
---

View File

@@ -0,0 +1,155 @@
# VKIST MSK Workspace (FR-26): Full System Use Case Specification
This document provides a comprehensive, unified, and standard-aligned specification of the **VKIST MSK Workspace (FR-26)** use-case model — **Patient Responsibility Dashboard** on **Mobile Web**. It acts as the definitive guide to the system-level architecture of the patient responsibility list view, showing how the 3 core use cases are structurally organized and how they implement the clinical responsibility tracking workflow for Diagnostic Radiologists.
The use-case design supports efficient patient management and ensures continuity of care by providing the clinical specialist with a structured, real-time view of all patients under their diagnostic responsibility.
---
## 1. Primary System Actors
The Patient Responsibility Dashboard coordinates interactions between the clinical specialist and the air-gapped hospital backend:
1. **UP5 (Diagnostic Radiologist):** The primary human actor. A Vietnamese clinical specialist managing a roster of musculoskeletal (MSK) patients, responsible for monitoring prescription status, diagnostic outcomes, and active treatment workflows across their assigned caseload.
2. **System (Backend):** The air-gapped hospital backend serving patient roster data, responsible for enforcing authentication, filtering patient lists, and routing navigation to consolidated clinical workspaces.
---
## 2. Functional Requirement Overview (FR-26)
| Field | Value |
| :--- | :--- |
| **FR-ID** | FR-26 |
| **FR Name** | VIEW PATIENT LIST WITH RESPONSIBILITY DETAILS |
| **Platform** | Mobile Web (PWA) |
| **Component** | Patient Responsibility Dashboard |
| **User Profile** | UP5 (Diagnostic Radiologist) |
| **UC-IDs** | UC-48377, UC-48378, UC-48379 |
| **Precondition** | User has valid authentication and access to patient management module. |
| **Postcondition** | System displays a list of patients the user is responsible for with details on prescriptions, diagnoses, and current work status, enabling efficient patient management and ensuring continuity of care. |
| **Trigger** | When user clicks the 'View Patient List' button, the system displays a comprehensive list of all patients for whom the user has responsibility, including their prescription status, diagnosis details, and current treatment status. This enables efficient patient management and ensures continuity of care. |
| **Engineer** | Đạt Trần Tiến (Daves Tran) |
| **Mobile Web** | Yes |
| **Desktop Web** | No |
| **Native Mobile** | No |
---
## 3. Complete Traceability Matrix
| UC-ID | Title [Verb + Noun] | Primary Actor | System Goal | Interaction Type | Relationship |
| :--- | :--- | :--- | :--- | :--- | :--- |
| **UC-48377** | View Patient List with Clinical Summary | UP5 (Diagnostic Radiologist), System | Access a structured list of patients under the user's clinical responsibility to monitor active diagnostic cases and treatment tracks. | User-to-System | Core (Entry Point) |
| **UC-48378** | Filter Patient List by Clinical Status | UP5 (Diagnostic Radiologist), System | Narrow the patient roster to a specific diagnostic or treatment sub-population to reduce cognitive load during high-volume clinical sessions. | User-to-System | Extends UC-48377 |
| **UC-48379** | Navigate to Patient Detail Record | UP5 (Diagnostic Radiologist), System | Drill into a specific patient's consolidated clinical workspace to review full diagnostic history, AI-generated overlays, and treatment context before rendering a final judgment. | User-to-System | Includes UC-48377 (from list) |
---
## 4. PlantUML Architectural Model
The following diagram defines the physical and logical boundaries of the **VKIST MSK Workspace (FR-26) — Patient Responsibility Dashboard**. It maps actor associations, internal system boundaries, and precise functional relationships (`<<include>>` and `<<extend>>` stereotypic dependencies).
```plantuml
@startuml
skinparam packageStyle rectangle
left to right direction
' ##########################################
' ACTORS DEFINITION
' ##########################################
actor "UP5\n(Diagnostic Radiologist)" as up5
actor :System\n(Backend): as sys <<system>>
' ##########################################
' VKIST MSK WORKSPACE SYSTEM BOUNDARY
' ##########################################
rectangle "VKIST MSK Workspace System (FR-26)\nPatient Responsibility Dashboard" {
usecase "UC-48377\nView Patient List\nwith Clinical Summary" as uc_48377 #LightBlue
usecase "UC-48378\nFilter Patient List\nby Clinical Status" as uc_48378 #LightYellow
usecase "UC-48379\nNavigate to Patient\nDetail Record" as uc_48379 #LightGreen
}
' ##########################################
' RELATIONSHIPS & ASSOCIATIONS
' ##########################################
' --- UP5 Actor Connections ---
up5 --> uc_48377 : Loads patient roster\n& reviews summary cards
up5 --> uc_48378 : Applies status/scan type\n& date-range filters
up5 --> uc_48379 : Taps patient card\nto open detail workspace
' --- System Backend Connections ---
sys --> uc_48377 : Retrieves authenticated\nuser's patient roster
sys --> uc_48378 : Re-queries with\nfilter constraints
sys --> uc_48379 : Fetches consolidated\nclinical workspace
' --- Use Case to Use Case Interdependencies ---
' UC-48378 extends UC-48377 (filtering is an optional enhancement to viewing)
uc_48378 -.-> uc_48377 : <<extend>>\n(Optional filter action\non the active patient list)
' UC-48379 includes UC-48377 (navigation originates from the loaded patient list)
uc_48379 -.-> uc_48377 : <<include>>\n(Navigation always occurs\nfrom a loaded patient list)
' UC-48378 can also be triggered from a filtered list before navigation
uc_48378 -.-> uc_48379 : <<extend>>\n(Filtering may occur before\nnavigating to detail record)
@enduml
```
---
## 5. Architectural Analysis of Functional Relationships
### 5.1 Stereotyped Dependencies
- **`<<include>>` Dependencies:**
- **`UC-48379` includes `UC-48377`:** Navigating to a patient detail record is always performed from a loaded patient list. The diagnostic radiologist must first access the roster (`UC-48377`) before tapping a patient card to open the consolidated workspace (`UC-48379`).
- **`<<extend>>` Dependencies:**
- **`UC-48378` extends `UC-48377`:** Filtering the patient list by clinical status (scan type, diagnostic grade, date range, workflow stage) is not a mandatory step in the core patient list loading workflow. It only occurs when the radiologist actively applies one or more filter constraints to narrow the roster.
- **`UC-48378` extends `UC-48379`:** When navigating from a filtered list view, the filter state may be preserved or reset depending on the clinical workflow context. The filtering action can be applied both on the main roster (`UC-48377`) or prior to drilling into a detail record (`UC-48379`).
### 5.2 Interaction Flow Summary
```
[UP5] ─── taps "My Patients" ────▶ [UC-48377: View Patient List]
├─── (optional) ──▶ [UC-48378: Filter Patient List]
│ ↑ │
│ └────────────────────┘
│ <<extend>>
└─── taps patient card ──▶ [UC-48379: Navigate to Patient Detail]
└────── <<include>> ──┘
```
### 5.3 Key Design Decisions
1. **Mobile-First List Architecture:** The patient list is designed for rapid vertical scrolling on constrained mobile viewports. Each card entry is intentionally condensed to display only essential clinical metadata (anonymized ID, scan type, status tag, prescription summary, workflow stage).
2. **Client-Side Filtering with Server Re-Query:** Filter operations send updated constraints to the backend rather than filtering in-memory, ensuring the server always holds the authoritative patient roster state and supporting pagination at scale.
3. **Decree 13 Compliance:** All patient data displayed in the list and detail views passes through the Decree 13 scrubbing layer before rendering. No raw PII (patient name, national ID, contact info) appears in the Mobile Web PWA.
4. **Single-Tap Navigation:** The radiologist navigates from list to detail with a single card tap, minimizing navigation depth to maintain rapid clinical workflow pace.
---
## 6. UC-ID Summary Table
| UC-ID | Purpose | Trigger | System Response |
| :--- | :--- | :--- | :--- |
| **UC-48377** | Load and display the authenticated radiologist's full patient roster with clinical summary fields (anonymized ID, scan type, diagnostic status, prescription summary, workflow stage). | UP5 taps "My Patients" / "View Patient List" button on the Mobile Web PWA dashboard. | System validates session, queries patient roster endpoint, renders scrollable paginated list of patient cards. |
| **UC-48378** | Narrow the visible patient roster to a specific diagnostic sub-population using filter constraints (status, scan type, date range) to reduce cognitive load. | UP5 applies one or more filter chips or dropdown selections to the currently displayed patient list. | System re-queries roster with filter parameters, updates list in-place, displays active filter chips for visual feedback. |
| **UC-48379** | Open a specific patient's consolidated clinical workspace to review full longitudinal diagnostic history, AI-generated GradCAM overlays, prescription and diagnosis summaries, and active treatment protocol notes. | UP5 taps a specific patient card entry from the roster (filtered or unfiltered). | System fetches the patient's full clinical workspace, validates Decree 13 scrubbing, renders consolidated detail view with longitudinal history and AI overlays. |
---
## 7. Alignment with Sprint 1-2 Architecture
FR-26 is a **Mobile Web (PWA)** feature within **Sprint 1-2 ("Fast PoC Baseline" → "Multi-Modal & NLP Integration")**, running on an air-gapped K3s cluster with a React/FastAPI/Triton stack. The patient list and detail views are the primary entry points into the broader MSK diagnostic workspace, which includes the scan loading (`UC-48376`), grading review (`UC-47988`), and report finalization (`UC-92006`) pipelines defined under FR-25.
---
(End of file — Full System Use Case Specification for VKIST MSK Workspace FR-26)

View File

@@ -0,0 +1,653 @@
# Sprint 1-2 Architecture Specification
**Sprint:** Sprint 1 (June 15 June 26) + Sprint 2 (June 29 July 10)
**Theme:** "The Fast PoC Baseline" + "Multi-Modal & NLP Integration"
**Parent Document:** [SOLUTION_ARCHITECTURE_SPEC.md](../SOLUTION_ARCHITECTURE_SPEC.md)
---
## 1. Sprint Scope & Goals
### 1.1 Sprint 1: The Fast PoC Baseline
- Establish interactive end-to-end processing pipeline
- Rapid UI prototype with high-fidelity mockups
- Configure initial FastAPI server pipelines to process raw array matrices
- Output early inference mask previews onto browser-based preview window canvas
- Wire ultrasound image ingest pathways into classification pipeline
### 1.2 Sprint 2: Multi-Modal & NLP Integration
- Embed localized NLP translation modules
- Implement client-side privacy scrubbing masks for Decree 13 compliance
- Configure on-device string parsers to sanitize personal identification tokens
- Deploy multi-modal diagnostic chat system
- Build zero-friction NLP conduits for clinical abbreviations
---
## 2. Architectural Constraints for Sprint 1-2
| Constraint | Implementation |
| --- | --- |
| **Air-Gapped Hospital LAN** | No external cloud processing during normal operation. All inference and storage on-premise via K3s. NFR-16a governs emergency cloud fallback only. 3-tier LLM system: Edge Gemma (browser WebLLM primary) → Gemini (GCP, orchestration/translation) → MedGemma (Modal, clinical deep-reasoning) → MOH templates. |
| **Code & Issue Hosting (NFR-16a exception)** | GitLab CE and Jira run self-hosted on a cloud VM (not SaaS). All code, tickets, and pipeline configs transit over public internet to reach the VM. Compensating controls: no PHI in commits or tickets (pre-push hook + team policy); SSH-only git access with certificate pinning; daily GitLab RDB backup to hospital MinIO (not cloud storage); cloud VM IAM with minimum-role access and 2FA; VM access restricted to hospital whitelisted IPs. This exception is reviewed at PoC sign-off. |
| **Network Bandwidth (10 Mbps max)** | Optimize payload sizes, leverage local caching (IndexedDB, Redis). |
| **DICOM Compliance** | Process DICOM images via pydicom stream over local HTTPS. |
| **Client Memory ≤ 150 MB** | PWA uses React + Zustand + LiteRT (no dedicated GPU required). |
| **Latency ≤ 1.5s for inference** | Heavy ML offloaded to Triton Inference Server with ONNX/TensorRT. |
| **Zero PHI Leakage** | Client-side scrubbing before network transfer; no PHI in Git/Jenkins. |
---
## 3. System Context Diagram (C4) - Sprint 1-2 Scope (FR-25 Synovitis Grading)
```plantuml
@startuml "VKIST_MSK_Sprint1_2_Context_FR25"
!include <C4/C4_Context>
title System Context - Sprint 1-2: FR-25 Synovitis Grading & NLP Integration
' Sprint 1-2 scope: FR-25 only with NLP safety layer
' Active Users: UP5 | Governance: UP1, UP4 | Future: UP6, UP7, UP8
' === ACTORS IN SPRINT SCOPE ===
Person(radiologist, "Diagnostic Radiologist (UP5)", "FR-25: load scan, review AI grade (0-3), confirm/override, finalize & sign, view GradCAM, engage circuit breaker, review RAG evidence")
' === GOVERNANCE / NFR ALIGNMENT ===
Person_Ext(senior_expert, "Healthcare Senior Expert (UP1)", "Governance: clinical protocol validation, MOH guideline approval, model threshold sign-off")
Person_Ext(support_staff, "Support Staff (UP4)", "Registration, case queue management")
' === FUTURE SPRINTS ===
Person_Ext(therapist, "Physical Therapist (UP6)", "Future sprint: prescription parser, 3D mapping, kinetic overlay, patient education")
Person_Ext(ortho_surgeon, "Orthopedic Surgeon (UP7)", "Future sprint: treatment planning, aggregated dashboard, 3D anatomy education")
Person_Ext(patient_caregiver, "Patient & Caregiver (UP8)", "Future sprint: self-monitoring portal, inflammation tracker, medication reminders")
Person(admin, "System Administrator", "K3s deployment, model updates, NGINX failover, Prometheus/Grafana monitoring")
' === SYSTEM ===
System(mpps, "VKIST MSK Processing System\n(FR-25 Sprint 1-2)", "Knee ultrasound AI: angle→inflammation→segmentation\nGrading: synovitis 0-3 with GradCAM\nSafety: Circuit Breaker, BERT drift, RAG-Referee\nNLP: Decree 13 scrubbing, GemmaE2B/MedGemma explanations\nReporting: Circular 46 PDF")
' === EXTERNAL SYSTEMS IN SCOPE ===
System_Ext(pacs, "Hospital PACS / Ultrasound", "DICOM source (C-MOVE + direct capture)")
System_Ext(emr, "Hospital EMR/HIS", "Finalized reports, audit logs (HL7/FHIR)")
System_Ext(triton, "Triton Inference Server", "GPU inference: angle, inflammation, segmentation + embeddings")
System_Ext(knowledge, "Clinical Knowledge Stack", "ladybugDB (ontology), pgvector (MOH guideline vectors in Postgres HNSW), EmbeddingGemma (RAG embedding model), Edge Gemma (browser WebLLM), Gemini (GCP Vertex AI orchestration), MedGemma (Modal clinical reasoning)")
' === RELATIONSHIPS ===
Rel(radiologist, mpps, "Load scan, review grade, confirm/override, finalize, view explanations, engage safety dialog", "HTTPS")
Rel(admin, mpps, "Deploys, monitors, configures", "HTTPS/SSH")
Rel(senior_expert, mpps, "Validates protocols, approves thresholds", "HTTPS")
Rel(support_staff, mpps, "Registration, case queue", "HTTPS")
Rel(therapist, mpps, "Not in Sprint 1-2 scope", "—")
Rel(ortho_surgeon, mpps, "Not in Sprint 1-2 scope", "—")
Rel(patient_caregiver, mpps, "Not in Sprint 1-2 scope", "—")
Rel(mpps, pacs, "DICOM import", "C-MOVE + upload")
Rel(mpps, emr, "Finalized reports, audit", "HL7/FHIR")
Rel(mpps, triton, "ML inference", "gRPC :8001")
Rel(mpps, knowledge, "RAG + ontology + LLM", "gRPC/HTTP + C++")
@enduml
```
---
## 4. Container Diagram (C4) - Sprint 1-2 Implementation
```plantuml
@startuml "VKIST_MSK_Containers_Sprint1_2"
!include <C4/C4_Container>
title Container Diagram - VKIST MSK Platform (Sprint 1-2)
Person(radiologist, "Radiologist (UP5)", "Performs primary diagnosis, view validation, and severity grading.")
Person(therapist, "Physical Therapist (UP6)", "Observes scans, evaluates kinetic data, and structures physical rehabilitation plans.")
System_Boundary(hospital_lan, "Air-Gapped Hospital LAN (Max 10 Mbps)") {
Container(pwa, "React PWA Frontend", "React, TS, Zustand, LiteRT, MediaPipe, Dexie.js", "Interactive visualization, local WebAssembly inference for view-angle validation, encrypted IndexedDB cache, DICOM upload, edge guardrail (Transformers.js BERT + OpenRedaction + pii-filter) in dedicated WebWorkers")
Container(nginx, "NGINX Reverse Proxy + Keepalived", "NGINX 1.27, Keepalived", "SSL/TLS termination, single VIP, instant failover (<=2s), routes to active FastAPI node")
System_Boundary(backend_servers, "Application Server Cluster") {
Container(fastapi, "FastAPI Application Server", "Python, Uvicorn, SQLAlchemy, ReportLab", "API orchestration, angle classification routing, inflammation detection, segmentation post-processing, measurement, severity analysis, PDF report generation, client-side scrubbing validation")
System_Boundary(ml_inference, "ML Inference Stack (Sprint 1-2)") {
Container(triton, "Triton Inference Server", "NVIDIA Triton, ONNX, OpenVINO, TensorRT", "Executes 3-step ML pipeline: angle classification, inflammation detection, segmentation (UNet3+/DeepLabV3)")
ContainerDb(model_store, "Local Model Store", "ONNX/Torch weights on disk", "Stores model checkpoints mounted read-only into Triton")
}
System_Boundary(observability, "Observability (Sprint 1-2)") {
Container(prometheus, "Prometheus", "Prometheus", "Scrapes FastAPI and Triton metrics")
Container(grafana, "Grafana", "Grafana", "Operational dashboards for inference latency, GPU utilization, cache hit rate")
}
System_Boundary(data_stack, "Data Stack (Sprint 1-2)") {
ContainerDb(postgres, "PostgreSQL + PostGIS", "PostgreSQL", "Stores session metadata, EMR records, spatial markers, audit ledger")
ContainerDb(redis, "Redis Cache", "Redis", "Session state, JWT tokens, rate limiting, consult_mode state (tier_1..tier_4)")
ContainerDb(s3, "S3 Object Store (MinIO)", "MinIO", "DICOM-derived images, segmentation overlays, Grad-CAM heatmaps, report blobs")
}
System_Boundary(cloud_stack, "Cloud LLM Stack (NFR-16a governed, PoC only)") {
Container(cloud_gw, "Cloud LLM Gateway", "FastAPI", "Routes between Gemini (tier_2) and MedGemma (tier_3); NFR-16a redaction, consent, audit enforcement; cost guarding")
}
}
}
Rel(radiologist, pwa, "Uploads DICOM, views overlays, accepts/rejects grades")
Rel(therapist, pwa, "Reviews scan results, annotates feedback")
Rel(pwa, nginx, "HTTPS requests with encrypted DICOM payloads", "HTTPS (Port 443)")
Rel(nginx, fastapi, "Routes API requests", "HTTP/1.1 (Port 8000)")
Rel(fastapi, redis, "Session verification, rate limiting", "TCP (Port 6379)")
Rel(fastapi, postgres, "Metadata, audit ledger, spatial queries", "SQL/TCP (Port 5432)")
Rel(fastapi, s3, "Image and report blob storage", "S3 API")
Rel(fastapi, triton, "Offloads ML inference: angle, inflammation, segmentation", "gRPC (Port 8001)")
Rel(triton, model_store, "Reads model weights", "Local FS")
Rel(fastapi, cloud_gw, "Routes cloud LLM consult (NFR-16a)", "HTTP/1.1 (Port 8000)")
Rel(cloud_gw, redis, "Consult mode, consent, audit", "TCP (Port 6379)")
Rel(cloud_gw, postgres, "Audit log append", "SQL/TCP (Port 5432)")
Rel(prometheus, fastapi, "Request latency, error rate, DB pool", "HTTP /metrics")
Rel(prometheus, triton, "Model latency, VRAM, GPU utilization", "HTTP /metrics")
Rel(grafana, prometheus, "Dashboard queries", "HTTP (Port 9090)")
@enduml
```
---
## 5. Component Diagram (C4) - FastAPI Server Internals (Sprint 1-2)
```plantuml
@startuml "VKIST_MSK_Backend_Components_Sprint1_2"
!include <C4/C4_Component>
title Component Diagram - FastAPI Application Server (Sprint 1-2)
Container_Boundary(backend, "FastAPI Application Server") {
System_Boundary(api_routers, "API Routers") {
Component(analyze_router, "Analysis Router", "FastAPI Router", "/api/analyze - Main ML pipeline endpoint\nHandles angle model selection, inflammation detection, segmentation routing, measurement, severity analysis")
Component(save_router, "Save Router", "FastAPI Router", "/api/save - Patient data persistence\nSanitizes metadata, stores images and PDF reports")
Component(export_router, "PDF Export Router", "FastAPI Router", "/api/export-pdf - On-demand PDF generation")
Component(health_router, "Health Router", "FastAPI Router", "/api/health - Liveness probe")
Component(cloud_orchestrate_router, "Cloud Orchestrate Router", "FastAPI Router", "/api/cloud-orchestrate - Gemini proxy for orchestration/translation/UI tasks (NFR-16a governed)")
Component(cloud_consult_router, "Cloud Consult Router", "FastAPI Router", "/api/cloud-consult + /api/cloud-consult/stream - MedGemma proxy for clinical deep-reasoning (NFR-16a governed)")
}
System_Boundary(ml_pipeline, "ML Pipeline Components (Sprint 1-2)") {
Component(angle_classifier, "Angle Classifier", "PyTorch/ONNX", "Classifies ultrasound plane (med-lat, post-trans, sup-trans-flex, sup-up-long)\nModels: ConvNeXt-Tiny, DenseNet-121, ResNet-50, EfficientNet-B2, Swin-V2-S")
Component(inflammation_detector, "Inflammation Detector", "PyTorch/ONNX", "Binary classifier for inflammation presence\nModel: EfficientNet-B0 (2-class)")
Component(segmentation_engine, "Segmentation Engine", "PyTorch/ONNX", "Pixel-wise anatomical segmentation\nModels: DeepLabV3-ResNet50/101, UNet-ResNet101, EfficientFeedback, UNet3Plus-Attention")
Component(measurement_engine, "Measurement Engine", "Python/NumPy/OpenCV", "Calculates synovium thickness in mm, effusion metrics\nPIXEL_TO_MM = 45.0 / 655.0")
Component(severity_analyzer, "Severity Analyzer", "Python", "Classifies synovitis grade (0-3) with combined effusion+synovium scoring\nGenerates Vietnamese clinical descriptions")
Component(preprocessor, "Image Preprocessor", "Python/PIL/OpenCV", "CLAHE contrast enhancement, resize, normalize")
Component(overlay_renderer, "Overlay Renderer", "Python/PIL", "Creates color-coded segmentation overlays with measurement annotations")
}
System_Boundary(knowledge_stack, "Knowledge Stack (Sprint 2)") {
Component(nlp_scrubber, "Patient Data Scrubber", "Microsoft Presidio", "Re-verify edge redaction; refine residual PII; error if unresolvable")
Component(rag_coordinator, "RAG Coordinator", "Python", "Retrieves MOH guideline passages from pgvector (PostgreSQL HNSW index); mandatory pre-generation retrieval for all 3 LLM tiers (Edge Gemma, Gemini, MedGemma)")
Component(ontology_query, "Ontology Query Engine", "C++ bindings", "ladybugDB graph traversal for anatomical entity relationships")
Component(guardrail_edge, "Edge Guardrail", "Transformers.js BERT", "Client-side hallucination/mal-intention/scope-breach scoring in WebWorker; triggers session termination → routes to tier_2 (Gemini) or tier_3 (MedGemma) via FastAPI")
Component(referee, "RAG-Referee", "BERT classifier", "Server-side mandatory 3-axis citation contestant validation for MedGemma (tier_3) output; attribution, cohesion, factual contestant status")
Component(cloud_orchestrate, "Cloud Orchestrate Service", "Python/FastAPI", "Routes task_type to Gemini (tier_2) via Vertex AI; orchestration, translation, UI planning")
Component(cloud_consult, "Cloud Consult Service", "Python/FastAPI", "Routes task_type to MedGemma (tier_3) via Modal; clinical deep-reasoning, report finalization")
}
System_Boundary(reporting, "Reporting & Audit") {
Component(pdf_generator, "PDF Report Generator", "ReportLab", "Bilingual (VI/EN) medical reports per Circular 46/2018/TT-BYT")
Component(audit_logger, "Immutable Audit Logger", "SQLAlchemy/Postgres", "Append-only clinical decision and AI interaction logs\nPrevents UPDATE/DELETE via DB triggers")
}
System_Boundary(infrastructure, "Infrastructure") {
Component(cache_client, "Redis Client", "redis-py", "Session state, JWT tokens, rate limiting")
Component(storage_client, "S3 Client", "boto3", "DICOM images, overlays, reports, Grad-CAM heatmaps")
Component(db_client, "PostgreSQL Client", "SQLAlchemy", "Relational/spatial queries, audit ledger")
Component(triton_client, "Triton gRPC Client", "gRPC", "Inference offloading for heavy ML models")
}
}
' Internal relationships
Component(analyze_router, preprocessor, "Uploads image, applies CLAHE")
Component(analyze_router, angle_classifier, "Classifies angle plane")
Component(analyze_router, inflammation_detector, "Detects inflammation (if post-trans or sup-up-long)")
Component(analyze_router, segmentation_engine, "Runs segmentation (if inflammation detected)")
Component(analyze_router, measurement_engine, "Measures thickness (sup-up-long only)")
Component(analyze_router, severity_analyzer, "Grades synovitis severity")
Component(analyze_router, overlay_renderer, "Generates visualization overlay")
Component(save_router, nlp_scrubber, "Validates scrubbing before save")
Component(save_router, pdf_generator, "Triggers PDF generation")
Component(save_router, audit_logger, "Logs save action immutably")
Component(export_router, pdf_generator, "Generates on-demand PDF")
Component(rag_coordinator, triton_client, "RAG embedding extraction via Triton (EmbeddingGemma)")
Component(rag_coordinator, ontology_query, "Graph traversal for evidence")
Component(rag_coordinator, referee, "Mandatory RAG-Referee validation per axis")
Component(guardrail_edge, "Edge Guardrail", "Transformers.js BERT", "Scores hallucination/mal-intention/scope-breach in WebWorker")
Component(guardrail_edge, referee, "Violation signal → server-side referee gate")
' Infrastructure connections
Component(analyze_router, cache_client, "Session validation")
Component(analyze_router, storage_client, "Image/report I/O")
Component(analyze_router, db_client, "Metadata, audit")
Component(analyze_router, triton_client, "ML inference offload")
Component(cloud_orchestrate_router, cloud_orchestrate, "delegates")
Component(cloud_consult_router, cloud_consult, "delegates")
Component(cloud_orchestrate, cloud_orchestrate, "routes to Gemini")
Component(cloud_consult, cloud_consult, "routes to MedGemma")
Component(cloud_orchestrate, cache_client, "Consult mode, consent")
Component(cloud_consult, cache_client, "Consult mode, consent")
Component(cloud_orchestrate, db_client, "Audit log append")
Component(cloud_consult, db_client, "Audit log append")
Component(cloud_consult, referee, "validates output before SSE")
@enduml
```
---
## 6. Deployment Diagram (C4) - Sprint 1-2 Hospital LAN Runtime
```plantuml
@startuml "VKIST_MSK_Deployment_Sprint1_2"
!include <C4/C4_Deployment>
title Deployment Diagram - VKIST MSK Platform (Sprint 1-2)
Deployment_Node(hw, "Hospital On-Premise Hardware\n(Dell PowerEdge / Local Server)") {
Deployment_Node(k8s_alt, "K3s Runtime (Sprint 1-2)") {
Deployment_Node(nginx_node, "NGINX Container") {
Container(nginx_c, "NGINX 1.27 + Keepalived", "Reverse proxy, SSL, VIP failover")
}
Deployment_Node(fastapi_node, "FastAPI Container") {
Container(fastapi_c, "FastAPI Uvicorn", "Python app: analysis, save, export, NLP scrub, RAG")
}
Deployment_Node(prom_node, "Prometheus Container") {
Container(prom_c, "Prometheus", "Metrics collection")
}
Deployment_Node(grafana_node, "Grafana Container") {
Container(graf_c, "Grafana", "Dashboards")
}
}
Deployment_Node(vm_db, "Database / Storage VM") {
Deployment_Node(pg_node, "PostgreSQL + pgvector Node") {
ContainerDb(pg_c, "PostgreSQL + PostGIS + pgvector", "Primary DB, vector HNSW index, audit ledger")
}
Deployment_Node(redis_node, "Redis Node") {
ContainerDb(redis_c, "Redis", "Session cache, rate limit")
}
Deployment_Node(s3_node, "MinIO Node") {
ContainerDb(s3_c, "MinIO S3", "Object storage for images and reports")
}
}
Deployment_Node(triton_hw, "Inference Server (GPU Node)") {
Deployment_Node(triton_node, "Triton Container") {
Container(triton_c, "Triton Inference Server", "ONNX/TensorRT models, ensemble pipelines")
}
Deployment_Node(model_vol, "Model Volume") {
ContainerDb(model_c, "Model Weights", "Read-only mount: .pth / .onnx files")
}
}
Deployment_Node(cloud, "NFR-16a Governed Cloud (PoC Only)") {
Deployment_Node(gcp_node, "GCP Vertex AI") {
Container(gemini_c, "Gemini (GCP Vertex AI)", "Orchestration, translation, UI planning")
}
Deployment_Node(modal_node, "Modal Cloud") {
Container(medgemma_c, "MedGemma (Modal)", "T4 GPU, clinical deep-reasoning, keep-warm")
}
Deployment_Node(secret_node, "GCP Secret Manager") {
ContainerDb(secret_c, "gemini-api-key", "API key for Vertex AI access")
}
}
Deployment_Node(client, "Clinician Workstation / Tablet") {
ContainerDb(browser, "Browser PWA", "React, Service Worker, IndexedDB")
}
Deployment_Node(model_vol, "Model Volume") {
ContainerDb(model_c, "Model Weights", "Read-only mount: .pth / .onnx files")
}
}
}
Deployment_Node(client, "Clinician Workstation / Tablet") {
ContainerDb(browser, "Browser PWA", "React, Service Worker, IndexedDB")
}
Rel(browser, nginx_c, "HTTPS (port 443)", "TLS 1.3")
Rel(nginx_c, fastapi_c, "HTTP (port 8000)", "Internal LAN")
Rel(fastapi_c, pg_c, "SQL/TCP (port 5432)", "LAN")
Rel(fastapi_c, s3_c, "S3 API", "LAN")
Rel(fastapi_c, triton_c, "gRPC (port 8001)", "LAN (or same host)")
Rel(fastapi_c, gemini_c, "Cloud LLM orchestration (NFR-16a redacted)", "HTTPS 443 (consent + audit)")
Rel(fastapi_c, medgemma_c, "Cloud LLM clinical reasoning (NFR-16a redacted)", "HTTPS 443 (consent + audit)")
Rel(prom_c, fastapi_c, "/metrics scrape", "HTTP")
Rel(prom_c, triton_c, "/metrics scrape", "HTTP")
Rel(graf_c, prom_c, "Dashboard", "HTTP")
Rel(triton_c, model_vol, "Loads weights", "Read-only FS")
Rel(browser, browser, "Local-only WebAssembly inference (LiteRT/MediaPipe)", "No network")
@enduml
```
---
## 7. Sprint 1-2 ML Workflow: Ultrasound Processing Pipeline
```plantuml
@startuml "VKIST_MSK_ML_Pipeline_Sprint1_2"
@startuml
title ML Pipeline - Knee Ultrasound Analysis (Sprint 1-2)
start
:Clinician uploads DICOM via PWA;
:PWA encrypts payload, sends over HTTPS;
:NGINX routes to active FastAPI node;
:FastAPI receives image;
:Apply CLAHE preprocessing (contrast enhancement);
:RUN ANGLE CLASSIFICATION;
:Model: ConvNeXt/DenseNet/ResNet/EfficientNet/Swin-V2;
:Classes: med-lat, post-trans, sup-trans-flex, sup-up-long;
if (Angle class?) then (post-trans)
: Inflammation Detection;
:Model: EfficientNet-B0 (binary);
if (Inflammation?) then (yes)
:Post-Trans Segmentation;
:Model: DeepLabV3-ResNet101;
:Classes: fat, tendon, muscle, femur, artery, synovium, baker's cyst;
:Generate segmentation masks;
:Create overlay image;
else (no)
:Skip segmentation;
:Set severity = 0;
endif
elseif (sup-up-long)
: Inflammation Detection;
if (Inflammation?) then (yes)
:Suprapatellar Segmentation;
:Model: UNet3Plus-Attention / DeepLabV3 / EfficientFeedback;
:Classes: effusion, fat, fat-pat, femur, synovium, tendon;
:Generate segmentation masks;
:Measure Synovium Thickness;
:ROI: middle 1/3 of bounding box;
:Unit: mm (PIXEL_TO_MM = 45/655);
:Create overlay with measurement annotations;
:Analyze Severity;
:Combined score: effusion (60%) + synovium (40%);
:Grade: 0 (Rất nhẹ) / 1 (Nhẹ) / 2 (Trung bình) / 3 (Nặng);
else (no)
:Skip segmentation and measurement;
:Set severity = 0;
endif
else (other angles)
:Only angle classification result returned;
endif
:Generate enhanced image (CLAHE);
:Assemble JSON response with overlays, masks, measurements, severity;
if (Save requested?) then (yes)
:NLP Scrubber validates Decree 13 compliance;
:Store images to MinIO S3;
:Store metadata to PostgreSQL;
:Append immutable audit log;
:Generate bilingual PDF report;
endif
:Return response to PWA;
stop
@enduml
```
---
## 8. Data Flow Diagram (Activity View)
```plantuml
@startuml "VKIST_MSK_DataFlow_Sprint1_2"
title Data Flow - VKIST MSK Platform (Sprint 1-2)
|PWA Client|
start
:Upload DICOM image;
:Apply CLAHE preprocessing locally;
:Encrypt payload;
:Send HTTPS POST to /api/analyze;
|NGINX Gateway|
:Receive request;
:SSL termination;
:Route to active FastAPI node;
|FastAPI Server|
:Receive image;
:Validate JWT + RBAC;
:Load angle classifier model;
:Run angle classification;
if (Angle class?) then (post-trans)
:Load inflammation detector;
:Run inflammation detection;
if (Detected?) then (yes)
:Load post-trans segmentation model;
:Run DeepLabV3-ResNet101 inference via Triton;
:Receive masks from Triton;
:Create segmentation overlay;
:Return JSON with overlay + masks;
else (no)
:Return JSON with angle + inflammation only;
endif
elseif (sup-up-long)
:Load inflammation detector;
:Run inflammation detection;
if (Detected?) then (yes)
:Load suprapatellar segmentation model;
:Run UNet3Plus-Attention inference via Triton;
:Receive masks from Triton;
:Measure synovium thickness (ROI middle 1/3);
:Calculate effusion metrics;
:Analyze severity (0-3 combined score);
:Create overlay with measurement annotations;
:Return JSON with overlay + measurement + severity;
else (no)
:Return JSON with angle + inflammation only;
endif
else (other)
:Return angle classification only;
endif
|PWA Client|
:Receive JSON response;
:Display enhanced image + overlays;
:Render measurement annotations;
:Show severity grade with color coding;
if (User action: Save?) then (yes)
|PWA Client|
:Run NLP scrubber (Decree 13);
:Remove PII from metadata;
:Send HTTPS POST to /api/save;
|FastAPI Server|
:Validate scrubbing compliance;
:Store images to MinIO S3;
:Store metadata to PostgreSQL;
:Append immutable audit log;
:Generate bilingual PDF report (Circular 46);
:Return success + folder path;
|PWA Client|
:Confirm save to user;
endif
if (User action: Socratic Consult?) then (yes)
|PWA Client|
:User queries AI about grade/severity;
:Run guardrail.worker.ts BERT check;
:Run OpenRedaction + pii-filter;
|FastAPI Server|
:Validate JWT + consent;
:Decree 13 redaction ground-check;
:Mandatory RAG pre-processing (pgvector top-k);
if (guardrail score?) then (PASS)
if (task_type?) then (orchestration/translation)
:Route to Gemini (tier_2) via Cloud LLM Gateway;
|Cloud LLM Gateway|
:Vertex AI Gemini generates response;
:Return SSE stream to PWA;
else (clinical_deep_reasoning)
:Route to MedGemma (tier_3) via Cloud LLM Gateway;
|Cloud LLM Gateway|
:Modal MedGemma generates response;
:RAG-Referee validates citation correctness;
:Return SSE stream to PWA;
endif
else (MITIGATE)
:Edge guardrail triggered cloud escalation;
:Fallback to MOH templates (tier_4);
:Return deterministic template response;
endif
|PWA Client|
:Display citation-footnoted response;
endif
stop
@enduml
```
---
## 9. Non-Functional Requirements Coverage (Sprint 1-2)
| NFR | Sprint 1-2 Implementation |
| --- | --- |
| **NFR-1** (DICOM Speed ≤ 3.0s) | Local Triton inference + Redis caching of recent sessions |
| **NFR-4** (Client Memory ≤ 150 MB) | React PWA with Zustand; LiteRT WebAssembly (no GPU); Dexie.js for local cache |
| **NFR-5** (Inference ≤ 1.5s) | Triton with TensorRT/OpenVINO quantization; ONNX runtime |
| **NFR-6** (VRAM ≤ 2 GB) | Quantized ONNX models; batch size 1; DeepLabV3-ResNet101 optimized |
| **NFR-7** (UI Refresh ≤ 200ms) | Token streaming via SSE; frontend state updates async |
| **NFR-8** (Fault Tolerance) | IndexedDB local cache; service worker offline mode; session recovery |
| **NFR-9** (Availability ≥ 99.9%) | NGINX + Keepalived active-passive VIP; Docker restart policies |
| **NFR-10** (Generative Safety) | 3-tier guardrail: prompt rules + BERT detection (hallucination/mal-intention/scope-breach) on Edge Gemma → mitigation to tier_2 (Gemini) or tier_3 (MedGemma) based on task_type. Mandatory RAG pre-processing for all 3 tiers (not optional tool calling). RAG-Referee citation contestant validation (3-axis) mandatory for MedGemma output. Server-side Decree 13 redaction ground-check before any cloud egress. |
| **NFR-11** (Onboarding ≤ 45 min) | High-fidelity Figma prototypes in Sprint 1; structured workflows |
| **NFR-13** (Grad-CAM Zero-Click) | Overlay rendered directly on upload; zero extra UI steps |
| **NFR-14** (No client GPU) | PWA falls back to CPU-bound rendering; LiteRT for lightweight inference |
| **NFR-16** (Air-Gapped) | Entire inference and storage stack on-premise via K3s; no external cloud for clinical data. NFR-16a exception: GitLab/Jira on cloud VM with compensating controls; cloud LLM (Gemini + MedGemma) permitted under redaction/consent/audit. |
| **NFR-17** (Immutable Audit) | PostgreSQL triggers prevent UPDATE/DELETE on audit tables; new event types for cloud LLM egress (egress_consent_gemini, egress_consent_medgemma, cloud_llm_escalation, egress_response) |
| **NFR-18** (RAG Citations) | pgvector retrieves MOH guideline passages (PostgreSQL HNSW index); mandatory pre-generation retrieval for all 3 LLM tiers (Edge Gemma, Gemini, MedGemma). RAG-Referee mandatory for MedGemma (tier_3) output |
| **NFR-19** (HITL Gate) | Database state machine prevents FINALIZED status without clinician digital signature |
---
## 10. Key Decree 13 / Circular 46 Compliance (Sprint 2)
### 10.1 Decree 13/2023/ND-CP - Personal Data Protection
- Client-side scrubbing via `nlp_scrubber` component before any network transfer
- Regex-based PII removal (names, IDs, phone numbers) in browser
- No PHI stored in Git, Jenkins artifacts, or external systems
### 10.2 Circular 46/2018/TT-BYT - EMR Compliance
- PDF reports generated per official MOH format (bilingual VI/EN)
- Audit trail immutable via database triggers
- Reports stored with cryptographic checksums in MinIO
---
## 11. Infrastructure Decisions (Sprint 1-2)
| Decision | Choice | Rationale |
| --- | --- | --- |
| **Orchestration** | K3s (Kubernetes-certified, lightweight edge distribution) | Chosen over Docker Compose, Docker Swarm, Nomad, ECS Fargate, Cloud Run. NFR-16 requires on-premise, eliminating cloud-only options. Docker Swarm offers lowest PoC cost but is in maintenance mode with highest migration overhead. K3s is already production-grade; scaling path is multi-cluster federation, not platform replacement. |
| **CI/CD** | Jenkins on hospital K3s | Runs inside trusted LAN; connects to cloud-hosted GitLab for source. No external build triggers. |
| **Code Hosting** | Self-hosted GitLab CE on cloud VM (NFR-16a exception) | Reliability over hospital-hardware self-hosting; not SaaS. Compensating controls: no PHI in commits (pre-push hook), SSH-only access with cert pinning, daily RDB backup to hospital MinIO, cloud IAM with 2FA + minimum-role, IP whitelist to hospital networks. Exception reviewed at PoC sign-off. |
| **Issue Tracking** | Self-hosted Jira on same cloud VM (NFR-16a exception) | Clinical feedback and tickets stay within controlled access boundary. No PHI in tickets per team policy. Same compensating controls as GitLab. |
| **Model Serving** | Triton Inference Server | ONNX/TensorRT support; ensemble pipelines; HTTP/gRPC |
| **Reverse Proxy** | NGINX + Keepalived | VIP for high availability; SSL termination; instant failover |
| **Local LLM** | Browser WebLLM (Edge GemmaE2B) + Gemini (GCP Vertex AI) + MedGemma (Modal) | Edge Gemma (Tier 1): Vietnamese + clinical language support, local inference (air-gapped), primary conversation, orchestrates cloud calls. Gemini (Tier 2): orchestration, translation, UI planning; NFR-16a governed. MedGemma (Tier 3): clinical deep-reasoning, report finalization; NFR-16a governed with RAG-Referee validation, <20% usage target. Triton hosts CV models + EmbeddingGemma only. |
| **Vector Search** | pgvector (PostgreSQL HNSW index) | Already deployed with Postgres; zero additional infrastructure; ~15K MOH vectors at ~5-20ms query latency fits NFR-7. Complex SQL filtering for clinical queries. Qdrant deferred to Phase 2. |
| **Ontology DB** | ladybugDB embedded | C++ library embedded in FastAPI process; SNOMED-CT/LOINC mappings |
---
## 12. Technology Stack Summary
| Layer | Technology | Purpose |
| --- | --- | --- |
| Frontend | React, TypeScript, Zustand, Dexie.js, LiteRT, MediaPipe, Transformers.js, OpenRedaction, pii-filter, js-data-anonymizer | PWA with offline support, local inference, and edge guardrail (BERT hallucination/mal-intention detection, Decree 13 PII scrubbing) |
| Guardrail | Transformers.js BERT (WebWorker), OpenRedaction, pii-filter, js-data-anonymizer, FastAPI `phi_scrub` middleware, Vertex AI Model Garden safety filters | Edge behavior control without NeMo/GuardrailsAI; prompt-rule + BERT detection; session termination + cloud mitigate; mandatory RAG pre-processing (not optional tool-call); server-side redaction ground-check before NFR-16a egress |
| Gateway | NGINX 1.27 + Keepalived | SSL termination, VIP, load balancing |
| API | FastAPI, Uvicorn, SQLAlchemy, ReportLab | REST API, ORM, PDF generation |
| ML Inference | Triton, ONNX, TensorRT, OpenVINO | Model serving with quantization |
| Models | ConvNeXt, DenseNet, ResNet, EfficientNet, Swin-V2, UNet3Plus, DeepLabV3 | Angle, inflammation, segmentation |
| Data | PostgreSQL + PostGIS, MinIO, Redis | Relational, object, cache |
| Knowledge | pgvector, ladybugDB, Edge Gemma, Gemini, MedGemma, EmbeddingGemma, BioClinicalBERT | RAG (pgvector HNSW), ontology (ladybugDB), 3-tier LLM (Edge Gemma primary Gemini orchestration MedGemma clinical), 768-dim RAG embeddings, BERT drift/referee |
| Cloud | Cloud LLM Gateway, Modal (MedGemma T4), GCP Vertex AI (Gemini) | FastAPI gateway enforcing NFR-16a redaction/consent/audit; routes task_type to appropriate tier; cost guarding (MedGemma <20% target); progressive streaming (25s timeout) |
| Observability | Prometheus, Grafana | Metrics and dashboards |
| Code Hosting | Self-hosted GitLab CE (cloud VM, NFR-16a exception) | Source control, issue tracking, merge requests, container registry |
| CI/CD | Jenkins & Jira on hospital K3s | Build, test, deploy pipeline, and collect ticket; connects to cloud-hosted GitLab |
---
## 13. Cross-References
| Document | Relevance |
| --- | --- |
| [SOLUTION_ARCHITECTURE_SPEC.md](../SOLUTION_ARCHITECTURE_SPEC.md) | Main architecture spec, NFR definitions, pattern citations, trade-off analysis |
| [Backend Specification](CODEBASE/backend/spec/backend-spec.md) | FastAPI server internal design, API contracts, RAG coordinator |
| [Knowledge Stack Specification](CODEBASE/knowledge/spec/knowledge_spec.md) | Qdrant/ladybugDB schema, embedding models, LLM endpoints |
| [CI/CD Deployment Pipeline](Design_Material/CI_CD_docs/CI_CD_DEPLOYMENT_PIPELINE.md) | Jenkins pipeline, Docker Compose runtime, offline bundles, rollback |
| [DATA_SPEC.md](CODEBASE/data/spec/data_spec.md) | Data contracts, schema definitions |
| [CONTEXT_VISION_SCOPE.md](../../PROJ_LEVEL_READING/PLAN/CONTEXT_VISION_SCOPE.md) | Project vision, sprint timelines, user personas |
---
## 14. Design Decisions
| # | Decision | Alternatives Considered | Rationale |
| --- | --- | --- | --- |
| 1 | Triton for model serving | TorchServe, FastAPI direct | Triton supports ONNX/TensorRT ensembles, dynamic batching, and concurrent model execution |
| 2 | K3s over Docker Compose/Swarm/Nomad/ECS/Cloud Run | Docker Compose, Docker Swarm, Nomad, ECS Fargate, Cloud Run | NFR-16 requires on-premise, eliminating cloud-only platforms (ECS, Cloud Run). Docker Swarm is in maintenance mode with highest migration cost to production. K3s is already production-grade; scaling to N hospitals is multi-cluster federation, not platform replacement. |
| 3 | MinIO over AWS S3 | Direct filesystem, NFS | S3-compatible API allows future cloud migration; object versioning built-in |
| 4 | pgvector over Qdrant/Pinecone | Qdrant, Pinecone, Weaviate | Postgres already deployed; pgvector adds zero infrastructure overhead. At ~15K MOH guideline vectors, HNSW query latency (~5-20ms in shared_buffers) fits within NFR-7 budget. Qdrant advantage appears at millions of vectors or >500 QPS; not needed at PoC scale. Phase 2: introduce Qdrant if corpus exceeds ~100K vectors. |
| 5 | ladybugDB embedded | Separate graph DB service | Embedded C++ library reduces latency; no separate process to manage |
| 6 | 3-tier LLM system (Edge Gemma → Gemini → MedGemma) | Single cloud LLM, on-prem only | Edge Gemma provides zero-cost primary inference; Gemini handles lightweight orchestration/translation; MedGemma reserved for high-value clinical reasoning with <20% usage cap. Cost-optimal and latency-optimal distribution of work across tiers. |
| 7 | FastAPI Cloud LLM Gateway | Separate service per cloud provider | Single gateway enforces NFR-16a uniformly (redaction, consent, audit) and simplifies consult_mode state machine extension. Reduces surface area for compliance bugs. |
| 8 | Modal for MedGemma deployment | GCP Vertex AI for all cloud LLM | Modal provides serverless T4 GPU with keep-warm for low cold-start latency; simpler than managing Vertex AI custom model endpoints. Separate from Gemini (Vertex AI) to allow independent scaling and cost tracking. |
---
## 15. Open Questions / Future Work (Post Sprint 2)
| ID | Question | Target Sprint |
| --- | --- | --- |
| Q1 | How to handle concurrent clinician sessions with shared Triton GPU? | Sprint 3 |
| Q2 | Should model weights be versioned in PostgreSQL for A/B testing? | Sprint 4 |
| Q3 | How to integrate DICOM C-STORE for automatic PACS ingestion (currently C-MOVE only)? | Sprint 5 |
| Q4 | What is the minimum WebGPU requirement for Edge Gemma (GemmaE2B-Q4) on target tablets? | Sprint 3 |
| Q5 | Should Prometheus scrape endpoints be authenticated for NFR-17 audit compliance? | Sprint 3 |
| Q6 | When does NFR-16a formally retire at PoC sign-off and revert to permanent NFR-16? | PoC sign-off |