English version: [[Users/rubinette/Vaults/notes/content/posts/operations-hospital-collaboration-guide-en]]

1. 文件目的

本文件提供給 TAMS Backend、Hospital Integration、RMF/Robot Integration、QA 與維運團隊,說明目前 Modular Monolith 的實際程式邊界、Operations 與 Hospital 的互動方式,以及修改程式時必須共同遵守的規則。

本架構仍部署為單一 NestJS process、單一 npm package 與單一版本。模組之間使用 NestJS dependency injection 與記憶體內方法呼叫,不透過內部 HTTP、message broker 或額外 worker framework。

核心原則如下:

Hospital 決定「醫院流程要求什麼」,Operations 保證「機器人任務如何安全執行」,Adapters 處理「如何與外部技術系統溝通」。

2. 快速判斷:程式應該放在哪裡

需求所屬位置範例
通用任務、機器人或設施規則src/operations/<feature>任務取消、派工、機器人可用性、充電站查找
跨多個 Operations feature 的背景流程src/operations/workflowsQueue Worker、Robot Keeper
CSH/CMP/HIS 或醫院專屬流程src/csh-hospital緊急返回藥局、員工同步、藥品查詢
HTTP validation 與 response mappingsrc/product-api、src/csh-hospital/api 或現有 controllerDTO、guard、Swagger decorator
MongoDB、Redis、RMF、Axios、ROS、Socket.IOsrc/adapters 或既有 infrastructure layerrepository、RMF adapter、robot-control adapter
登入、JWT、RBAC、session、auditsrc/accessActor、permission、使用者管理
多個模組共同使用的純粹型別src/domain與單一 Operations feature 無關的共用型別

若需求包含醫院名稱判斷,例如 if (hospital === 'CSH'),不要直接加入 Operations。先確認差異是否能由 Hospital config 表達;只有行為真的不同時,才建立小而明確的跨模組契約。

3. 整體架構

flowchart LR
  ProductAPI[Product API controllers]
  HospitalAPI[Hospital API controllers]
  HospitalWF[CSH Hospital workflows]

  subgraph Operations[Operations Module]
    Task[Task feature]
    Robot[Robot feature]
    Fleet[Fleet feature]
    Facility[Facility feature]
    Settings[Settings feature]
    CrossWF[Queue Worker / Robot Keeper]
  end

  SharedPorts[Operations shared ports]
  LocalPorts[Feature-local outbound ports]
  Adapters[Mongo / Redis / RMF / Robot control / WebSocket adapters]

  ProductAPI -->|feature public entry| Task
  ProductAPI -->|feature public entry| Robot
  ProductAPI -->|feature public entry| Fleet
  ProductAPI -->|feature public entry| Facility
  ProductAPI -->|feature public entry| Settings

  HospitalAPI --> HospitalWF
  ProductAPI -->|medicine-cart endpoint| HospitalWF
  HospitalWF -->|TaskProgressionService public entry| Task
  HospitalWF -->|EmergencyOperationsPort| SharedPorts
  SharedPorts -->|implemented by emergency facade| CrossWF
  HospitalWF -->|SiteOperationsConfig implementation| SharedPorts

  Task --> LocalPorts
  Robot --> LocalPorts
  Fleet --> LocalPorts
  Facility --> LocalPorts
  Settings --> LocalPorts
  CrossWF --> Task
  CrossWF --> Robot
  LocalPorts -->|implemented by| Adapters
  SharedPorts -->|events/config adapters| Adapters

3.1 依賴方向

正常方向為:

API / Hospital workflow
        ↓
Operations application
        ↓
Domain + Ports
        ↑
Adapters implement Ports

Operations 不得 import CSH Hospital 的具體實作。Hospital 透過 task feature 的 public 入口取得通用 task capability,並透過 EmergencyOperationsPort 執行跨 task/robot/fleet 的完整 Operations 用例。Operations 回傳中性的結果,不回呼 Hospital。

4. Operations 的 feature-first 結構

src/operations/
├── task/
│   ├── domain/
│   ├── application/
│   │   └── ports/
│   ├── public/
│   └── task.module.ts
├── robot/
│   ├── domain/
│   ├── application/
│   │   └── ports/
│   ├── public/
│   └── robot.module.ts
├── fleet/
├── facility/
├── settings/
├── workflows/
├── ports/
└── operations.module.ts

4.1 Feature 內各層責任

domain

  • 保存 business entity、enum、record、value object 與純規則。
  • 不得依賴 NestJS、Swagger、MongoDB/Mongoose、Redis、Axios、ROSLIB 或 Socket.IO。
  • 不得出現 HTTP request、Axios response、Mongoose document 或 raw RMF payload。
  • ID 在 domain/application/port 邊界一律使用字串。

application

  • 執行一個具體 use case,協調 domain 與 ports。
  • 可以使用 NestJS DI,但不得 import concrete adapter、API DTO 或 Hospital implementation。
  • 不直接組裝 Axios、RMF 或 Redis wire payload。

application/ports

  • 是該 feature 對外部資源的需求,例如 repository、runtime state store、RMF command 或 robot control。
  • Port 應以 use case 所需能力設計,避免 generic CRUD repository。
  • Port command/result 必須 transport-neutral。

public

  • 是 API、Hospital、CLI 或 inbound adapter 進入 feature 時的公開 import 入口。
  • 目前 public/index.ts 是受控的 export barrel,不是額外的 proxy 或獨立 service instance。
  • 新增 export 前應先確認外部模組真的需要它;不要把所有 application service 全部重新匯出。

*.module.ts

  • 註冊與 export 該 feature 的 providers。
  • Feature module 不放 HTTP controller。
  • OperationsModule 只組合 feature modules 與跨 feature workflows。

4.2 Feature 所有權

Feature負責內容主要對外能力
Tasktask history、task library、queue intent、dispatch/cancel/continueTaskApplicationService、TaskRequestService、TaskCancelService
Robotruntime state、availability、navigation、snapshot、videoRobotApplicationService、RobotVideoService、UpdateRobotStateService
Fleetfleet CRUD、fleet command、Fleet Manager discoveryFleetApplicationService、FleetService
Facilitymap、door/lift、device、charging lookupDeviceApplicationService、MapFloorService
Settings系統設定SystemSettingsApplicationService

TaskQueueWorker 與 RobotKeeperWorker 同時依賴多個 feature,因此放在 operations/workflows,並由 OperationsModule 註冊。

5. CSH Hospital 模組

src/csh-hospital/
├── api/           Hospital HTTP controllers
├── config/        CSH/site 設定與 typed access
├── dto/           Hospital transport DTO
├── integrations/  CSH/CMP/HIS HTTP integration
├── ports/         Hospital 自己的 persistence/integration ports
├── workflows/     Hospital business workflows
└── csh-hospital.module.ts

Hospital 模組負責:

  • 解讀 CMP/HIS/CSH 的 request 與事件語意。
  • 員工、藥品與醫院資料整合。
  • 決定緊急事件下要採取的醫院業務流程。
  • 透過 Operations 能力取消、建立、暫停或繼續任務。
  • 提供站點與醫院設定,但不讓 Operations 直接判斷醫院名稱。

Hospital 不應負責:

  • 建立 RMF payload 或直接呼叫 Open-RMF。
  • 直接操作 Redis key 或 Mongo collection。
  • 自行重做 queue lease、robot availability、dispatch idempotency。
  • 繞過 Operations 的取消與派工安全規則。

6. Operations 與 Hospital 的溝通方式

6.1 Hospital 呼叫 Operations

Hospital controller 先呼叫 Hospital workflow,再由 workflow 使用 Operations 提供的 task/robot 能力。

以緊急事件為例:

  1. POST /cmp/messages/hospital 收到 ReturnToErPharmacy。
  2. HospitalMissionControlService.requestForActiveTasks() 先將全 fleet 的 taskAcceptancePausedAt 與新的 taskAcceptancePauseVersion 寫入 MongoDB。
  3. 透過 EmergencyOperationsPort 取得需要處理的 active task contexts;若 MongoDB 或 Redis 讀取失敗,流程 fail closed,已寫入的 fleet pause 保持有效。
  4. 對所有機器人廣播 EMERGENCY_STARTED,而不是只通知 active robot。
  5. 對每個 active task 判斷:
    • 機器人正在移動:取消原任務,建立返回藥局任務。
    • 已抵達非藥局站點:記錄 return request,等待 HMI continue。
  6. 單一 task 的返程失敗不阻塞其他 task;該項回傳 EMERGENCY_RETURN_REQUIRES_RECOVERY。
  7. 回傳 notifiedRobots 與每個 task 的處理結果。emergency.changed 在 Mongo pause 成功後發布。

HospitalMissionControlService 只注入 EmergencyOperationsPort 與 Hospital config。Repository、Redis runtime state、HMI、取消安全規則及 queue 建立都封裝在 EmergencyOperationsService,避免 Hospital 知道 Operations 的資料操作細節。

6.2 Medicine-cart continue 的協調方式

HMI 呼叫 task continue 時,Product API 呼叫 MedicineCartTaskWorkflow。此 Hospital workflow 再呼叫 Operations 的 TaskProgressionService:

  • 一般任務:Operations 排入下一站並回傳 continued 結果。
  • 存在尚未處理的 returnToErPharmacyRequestedAt:Operations 回傳 emergency-return-required 與 transport-neutral task context。
  • Hospital 收到 emergency outcome 後決定返回站點,再透過 EmergencyOperationsPort.cancelAndQueueReturn() 建立返程,最後保持原 HTTP response shape。

Operations 的結果不包含 Hospital class、HTTP DTO 或 CSH 設定:

type TaskContinuationOutcome =
  | { kind: 'continued'; result: ContinueTaskResponse }
  | { kind: 'emergency-return-required'; task: EmergencyContinuationContext };

因此沒有 Operations → Hospital 的反向 dependency,也不需要 Hospital provider token 綁回 TaskModule。TaskProgressionService 由 TaskModule 正常註冊;MedicineCartTaskWorkflow 由 CshHospitalModule 註冊並提供給 controller。

6.3 設定傳遞

Operations 透過 SiteOperationsConfig 讀取:

  • siteName
  • returnHomeStation
  • taskSimulation

CshHospitalConfig 實作此契約,再由 AdaptersModule 使用 useExisting 綁定到 SITE_OPERATIONS_CONFIG。

若要增加 Operations 必需的 site-level 設定:

  1. 在 SiteOperationsConfig 加入 transport-neutral 欄位。
  2. 在 Hospital config 實作並保留相容預設值。
  3. 加入設定單元測試與 Operations use-case 測試。
  4. 更新 .env.example 與部署文件。

不要讓 Operations 直接注入 ConfigService 讀取 Hospital-specific env,也不要在 application module load 時直接讀 process.env。

6.4 業務事件發布

Operations 與 Hospital 透過 OperationsEventPublisher 發布:

  • robot.state.changed
  • task.state.changed
  • fleet.state.changed
  • emergency.changed

目前實作由 WebSocket adapter 接收。此 publisher 用於 dashboard/event notification,並不是具 durable delivery 的 event bus;不能用它取代必須成功的 MongoDB 狀態更新或 queue transaction。

7. 緊急返回藥局流程與不變條件

sequenceDiagram
  participant CMP
  participant HospitalAPI
  participant HospitalWF as HospitalMissionControl
  participant Ops as EmergencyOperationsPort
  participant Mongo
  participant HMI as RobotControlPort
  participant Queue as Task Queue Worker
  participant RMF as FleetCommandPort
  participant ReturnOp as EmergencyReturns

  CMP->>HospitalAPI: ReturnToErPharmacy
  HospitalAPI->>HospitalWF: requestForActiveTasks()
  HospitalWF->>Ops: pauseFleetTaskAcceptance()
  Ops->>Mongo: persist pause timestamp + version
  HospitalWF->>Ops: listActiveTaskContexts()
  Ops->>Mongo: load active tasks and ownership
  Ops->>Ops: read normalized Redis runtime state
  HospitalWF->>Ops: broadcastEmergency(EMERGENCY_STARTED)
  Ops->>HMI: notify every robot
  loop each active task
    alt robot is moving
      HospitalWF->>Ops: cancelAndQueueReturn()
      Ops->>ReturnOp: claim by original task id
      Ops->>ReturnOp: phase=CANCELLATION_REQUESTED
      Ops->>RMF: cancel booking
      RMF-->>Ops: explicitly accepted
      Ops->>Mongo: cancel original task locally
      Ops->>ReturnOp: phase=ORIGINAL_CANCELED
      Ops->>Mongo: create return task + queue intent
      Ops->>ReturnOp: COMPLETED / RETURN_QUEUED
    else arrived at non-pharmacy station
      HospitalWF->>Ops: markReturnRequested()
      Ops->>Mongo: persist return request + emergency version
    end
  end
  HospitalWF-->>CMP: affectedTasks + notifiedRobots
  Queue->>RMF: dispatch return task safely

修改此流程時必須維持:

  • Fleet pause 必須阻擋普通任務與 Robot Keeper 自動充電任務。
  • Manual cancel 在 active robot 被 emergency pause 時仍拒絕。
  • 只有 emergency return 內部流程可使用 allowDuringEmergency: true。
  • Emergency return 必須同時使用 requireFleetConfirmation: true;只有 RMF 明確接受取消後,才可刪除本地 queue、取消 task history 並建立返程。
  • RMF 取消遭拒或結果不明時不得自動重試或建立返程,必須保留 durable operation 供人工 reconciliation。
  • 緊急返回取消原任務時使用 notifyAmr: false,避免一般 CANCELED 畫面覆蓋緊急畫面。
  • EMERGENCY_STARTED 與 EMERGENCY_CLEARED 要通知所有機器人狀態,不只 active robots。
  • 單一 HMI 通知失敗不能阻塞 fleet 流程;通知採 fire-and-forget failure isolation。
  • notifiedRobots 表示已嘗試通知的 robot names,不表示每台 HMI 已確認收到。
  • Emergency return 可越過沒有 TaskTracking 支撐的 stale reservation,但不能越過明確屬於其他 task 的 tracking。
  • Mission continues 先 capture 當下的 emergency versions,只清除相同版本的 return request 與 fleet pause;若期間出現新的 emergency version,不可清除新事件,且不可廣播 EMERGENCY_CLEARED。

7.1 Emergency version 的並行規則

  • 每次 ReturnToErPharmacy 產生新的 version;已暫停 robot 保留原始 taskAcceptancePausedAt,但更新成最新 version。
  • MissionContinues 先擷取當下所有 version,再以 conditional update 清除。擷取之後才開始的新 emergency 不符合條件,因此不會被舊的 clear request 清掉。
  • 舊資料沒有 version 時以 null legacy version 處理,讓升級後第一次 clear 仍可安全清除。
  • EMERGENCY_CLEARED 只有在確認已沒有任何 paused robot 後才發布與廣播。

7.2 Emergency return 的 idempotency 與恢復

MongoDB EmergencyReturns collection 以原始 taskHistoryId 作為唯一 _id。同一任務的並行 HMI continue 或重送事件只有一個 owner 可以執行;完成後的重送回傳同一個 returnTaskId。

statephase意義與處置
PROCESSINGCLAIMED尚未記錄取消意圖;若長時間停留,先查 backend log 與 Mongo 狀態
PROCESSING / FAILEDCANCELLATION_REQUESTEDRMF 取消可能未送出、遭拒或結果不明;先向 RMF 對帳,不可直接重放
PROCESSING / FAILEDORIGINAL_CANCELED舊版或尚未預配返程 ID 的流程;無法證明 crash 前未建立返程,只能人工對帳,不可由 CLI 重放
PROCESSING / FAILEDRETURN_ALLOCATED已先持久化 returnTaskId;TaskHistory/TaskQueue 可用同一 ID 冪等補齊
COMPLETEDRETURN_QUEUED返程已建立;returnTaskId 是後續追蹤依據

FAILED 不會由 API 自動重新 claim。值班人員必須確認 RMF booking、原 TaskHistory、robot ownership、return TaskHistory 與 TaskQueue 後,再依站點 runbook 決定人工補建返程或修正 operation。直接刪除 EmergencyReturns record 會重新開放 side effect,未完成對帳前禁止執行。

使用 yarn diagnose:emergency-returns 可列出超過五分鐘仍未完成的 operation,並一次呈現 operation、原任務、已知返程、queue 與 robot ownership。--stale-minutes 0 顯示全部 unresolved operations;--task-id <original-task-history-id> 可精確查詢單筆。此命令只讀 MongoDB,且固定輸出 automaticReplayAllowed: false;recommendedAction 是對帳分類,不是自動復原授權。

新版流程會在建立返程 TaskHistory 前先將預配的 returnTaskId 寫入 RETURN_ALLOCATED。只有 FAILED + RETURN_ALLOCATED 可以使用受控復原命令;命令會沿用相同 ID 補齊缺少的 TaskHistory 或 TaskQueue,不會重送 RMF cancellation。操作時必須從最新診斷結果複製精確 updatedAt,並提供 operator、reason 與確認旗標;Mongo conditional update 保證舊診斷或並行操作者無法取得 recovery ownership,結果會追加至 recoveryAudit。

8. 資料權威與一致性

資料權威來源注意事項
Task history 與 task ownershipMongoDBRedis/RMF event 不可直接取代歷史資料
Queue lifecycle、lease、attemptMongoDB必須透過原子 claim/update
taskAcceptancePausedAt、taskAcceptancePauseVersionMongoDBEmergency 的 fleet-wide gate 與 conditional clear
returnToErPharmacyRequestedAt、emergencyRequestVersionMongoDB等待 HMI continue 的 task 級 emergency request
Emergency return operationMongoDB EmergencyReturns原始 task idempotency、取消階段與 return task id
RMF status、battery、locationRedis runtime state有 TTL,可能 missing
fullCharging、obstacle telemetryRedis runtime stateGET robots 以 overlay 呈現
HMI busy leaseRedisAvailability 必須合併判斷
RMF booking reconciliationMongo task/queue + RMF inbound重複 inbound 必須 idempotent

Redis 回傳必須區分:

  • missing:目前沒有該機器人的 runtime state。
  • failure:Redis 讀取失敗。

派工、availability、auto-charge 與 emergency task evaluation 遇到 failure 必須 fail closed,不能把它當成 idle、available 或空狀態。Mongo repository 的讀取失敗也不得映射成「robot 不存在」。

9. Queue 與派工安全規則

Queue lifecycle:

PENDING → PROCESSING → DISPATCHING → IN_PROGRESS → COMPLETED / FAILED

共同維護時不可破壞:

  • Worker 只能處理 Mongo findOneAndUpdate 原子 claim 成功的 record。
  • Claim 時寫入 workerId、leaseExpiresAt 與 attemptCount。
  • RMF 呼叫前必須先持久化 DISPATCHING。
  • 明確 RMF rejection 可以依上限重試。
  • 網路結果不明時不可自動重派,避免同一任務在 RMF 重複建立。
  • 過期 PROCESSING 可安全重領;無法 reconciliation 的 DISPATCHING 進入明確失敗狀態。
  • taskHistoryId 是 idempotency root。
  • 多站任務重用同一 queue record,並增加 dispatchSequence。
  • Robot Keeper 建立 charging task 必須使用原子去重條件。
  • 普通任務沒有 docking map metadata 時仍應能派送;只有真正需要 docking activity 時才要求 charging metadata。

10. Dependency Injection 與 Port Binding

Port 使用 Symbol token 注入,而不是注入 concrete adapter class:

constructor(
  @Inject(FLEET_COMMAND_PORT)
  private readonly fleetCommand: FleetCommandPort,
) {}

Adapter module 使用 useExisting:

RmfFleetAdapter,
{
  provide: FLEET_COMMAND_PORT,
  useExisting: RmfFleetAdapter,
}

useExisting 的目的,是讓 token 與 concrete provider 指向同一 singleton,避免同一 adapter 被建立兩次。

新增 port 時應:

  1. 放在真正擁有需求的 feature application/ports。
  2. 定義 transport-neutral command/result。
  3. 在 adapter 實作它。
  4. 在 AdaptersModule 用 useExisting 綁定並 export token。
  5. 使用 fake port 或 mock 寫 application service tests。

只有同時被不同頂層模組使用、且代表跨模組協作的契約,才放入 operations/ports。

11. 常見修改情境

11.1 新增一般任務規則

  1. 將 invariant 放在 operations/task/domain 或 task application use case。
  2. 若需要 robot/fleet 資料,依賴既有 contract;不要 import Mongo repository class。
  3. 若需要 RMF 新能力,擴充 FleetCommandPort 與 RMF adapter。
  4. 加入 task service test,若涉及派工則補 queue/RMF adapter test。

11.2 新增醫院事件

  1. 在 Hospital DTO 驗證 transport request。
  2. 在 csh-hospital/workflows 解讀醫院事件。
  3. 呼叫 Operations 的公開能力完成任務/機器人操作。
  4. 若 Operations 缺少一個完整用例,先新增 use case;不要讓 Hospital 直接修改更多 repository 欄位。
  5. 保持既有 HTTP status、error code 與 response shape,除非 API 版本已正式調整。

11.3 新增另一家醫院

不要複製整個 Operations。建議建立新的 Hospital module,實作 site config 與自己的 orchestration workflow,再重用 EmergencyOperationsPort 與各 feature public capability。

新增醫院前應先盤點:

  • 站點命名與返回站點規則。
  • Emergency broadcast/HMI endpoint 差異。
  • 身分驗證與 HIS/CMP payload。
  • 藥品及員工資料來源。
  • Medicine-cart continue 是否使用不同的 Hospital policy/workflow。
  • AppModule 如何在單站部署中選擇唯一 Hospital module 與 site config。

11.4 新增或更換外部整合

  • Axios、ROS、Socket.IO client 放在 adapter。
  • Typed config 注入 adapter,不在 application 中讀 raw env。
  • Adapter 將外部錯誤映射為 application 可理解的 result/error。
  • Raw RMF payload 必須在 inbound adapter normalize 後,才交給 UpdateRobotStateService。

11.5 修改 HTTP DTO 或 route

  • DTO 留在 API layer,domain 不加 Swagger/class-validator decorator。
  • Application command/result 與 HTTP DTO 分開。
  • 保留既有路由、status code、error code、request/response shape;如果一定要 breaking change,必須另開 API version 並與 consumer team 協調。
  • 修改後要比較 Swagger paths、methods、status 與 schemas。

12. Import 規則

允許

// Controller 透過 feature public entry 使用 application capability
import { TaskRequestService } from '../operations/task/public';

// Application 注入自己的或其他 feature 的 contract
import {
  ROBOT_STATE_STORE,
  RobotStateStore,
} from '../../robot/application/ports/robot-state-store.port';

禁止

// Controller 穿透 feature internal application tree
import { TaskRequestService } from '../operations/task/application/task-request.service';

// Application 直接依賴 adapter
import { RmfFleetAdapter } from '../../../adapters/rmf/rmf-fleet.adapter';

// Domain 依賴 NestJS / Mongoose / Swagger
import { Injectable } from '@nestjs/common';

現有 .eslintrc.js 會檢查:

  • Domain 不得 import framework/infrastructure。
  • Application/ports/workflows 不得 import adapter、infra、Product API、Hospital implementation,或 Axios/Mongoose/Redis/ROSLIB/Socket.IO 等 concrete transport 套件。
  • Controller 不得 import repository、adapter、infra 或 feature internal application tree。

test/service/import-boundaries.service.spec.ts 會用刻意違規的 import 驗證 ESLint override;修改 override 順序或 glob 時必須執行此測試,避免較後面的 Hospital override 靜默覆蓋 controller 規則。

架構限制仍須由 reviewer 判斷語意;ESLint 只能檢查 import pattern,無法判斷一個 service 是否洩漏過多內部能力。

13. 現行邊界與後續收斂方向

13.1 Emergency facade 是跨模組唯一入口

HospitalMissionControlService 不再注入 feature-local repository/control ports。它只使用 EmergencyOperationsPort 的行為型能力:

  • listActiveTaskContexts
  • broadcastEmergency
  • pauseFleetTaskAcceptance / clearFleetTaskAcceptance
  • markReturnRequested / clearReturnRequested / markReturnHandled
  • cancelAndQueueReturn

EmergencyOperationsService 是此 port 的 Operations 實作,內部才協調 task、robot、fleet、runtime state、HMI 與 queue。新增 emergency 能力時,優先擴充完整業務操作,不要把 repository CRUD 暴露給 Hospital。

13.2 public/index.ts 是可執行的跨模組 import boundary

Hospital 可從 operations/task/public 使用 TaskProgressionService 等明確公開能力,不得 import operations/*/application/**。.eslintrc.js 已對 src/csh-hospital/**/*.ts 強制此規則。

13.3 Task progression 與 Hospital orchestration 分開註冊

TaskProgressionService 屬於通用 task application,由 TaskModule 註冊。MedicineCartTaskWorkflow 屬於 CSH Hospital orchestration,由 CshHospitalModule 註冊並 export 給 Product API controller。

不要把 Hospital policy 加回 TaskProgressionService,也不要讓 Operations 依賴 CshHospitalModule。新的分支結果應維持 transport-neutral,再由 Hospital workflow 解讀。

13.4 目前每個 deployment 使用一組 Hospital policy

目前 composition 預期每個部署選擇一個 Hospital module 與 SiteOperationsConfig。若單一 process 同時支援多家醫院,需要增加 site/hospital routing,不能共用單一全域 config token。

14. 測試與驗收

14.1 最低驗證矩陣

修改範圍必跑驗證
純 domain/value objectLint、build、該 feature unit tests
Application service上述項目 + service tests
Module/provider/public entry上述項目 + module boundary tests
Port/adapter上述項目 + adapter contract tests
Queue/emergency/runtime stateMongo/Redis integration tests + E2E
HTTP DTO/routeE2E + Swagger contract comparison
Deployment/configProduction image build + startup smoke test

14.2 標準命令

使用 pipeline 對齊的 Node.js 24.13.0:

yarn install --frozen-lockfile
yarn lint:check
yarn test:architecture
yarn build
yarn test test/service --runInBand
yarn test:integration --runInBand
yarn test:e2e --runInBand

Service integration 與 E2E 需要 MongoDB、Redis 和測試 .env。裸 checkout 缺少這些依賴時產生的連線失敗,不應被誤判為程式 regression,也不能被視為測試通過。

Jest 設定刻意不使用 forceExit。測試結果已輸出但程序沒有自行結束,仍視為 lifecycle failure;E2E 必須用 AppHelper.closeAgent() 關閉 Nest app,測試資料庫必須用 MongoHelper.close() 關閉底層 pool,新增的長時間 adapter/worker 也必須在 Nest shutdown hook 中中止或等待工作完成。

建議以 Docker Compose 驗證:

docker compose up -d mongodb redis
docker compose run --rm backend yarn test test/service --runInBand
docker compose run --rm backend yarn test:e2e --runInBand

實際 compose 設定必須讓 test container 連到 mongodb 與 redis service names,而不是 container 內的 localhost。

14.3 特別回歸案例

涉及 Operations/Hospital 時至少確認:

  • 普通單站任務沒有 docking metadata 仍能派送。
  • 多 worker 競爭只有一個 claim 成功。
  • Dispatch timeout/unknown result 不會重派。
  • Multi-station continuation 正確增加 sequence。
  • Duplicate RMF terminal event 不會重複完成。
  • Redis failure 使 dispatch/auto-charge fail closed。
  • Redis/Mongo read failure 使 emergency evaluation fail closed,且 fleet pause 保持有效。
  • Emergency pause 阻擋普通任務與 Keeper。
  • 舊 MissionContinues 不會清除較新的 emergency version。
  • 同一原始 task 的並行 emergency return 只有一次 cancel/create side effect。
  • RMF cancel rejected/timeout 不會刪除本地 queue 或建立返程;operation phase 可供對帳。
  • Emergency return stale reservation 規則正確。
  • Manual cancel 在 emergency pause 下仍被拒絕。
  • Fleet-wide HMI broadcast 保留所有狀態 robot 與 notifiedRobots。
  • Obstacle/full-charging telemetry overlay 未回歸。
  • Docking arrival decision-manager activity 與活動順序未改變。

15. Pull Request 協作流程

15.1 作者提交前

  • 已判斷需求的 feature/Hospital 所有權。
  • 未將醫院名稱或醫院專屬 env 判斷放入 Operations。
  • Controller 只做 validation、mapping、application 呼叫與 response mapping。
  • Application 未 import concrete adapter。
  • 新 port 使用字串 ID 與 transport-neutral type。
  • 沒有讓 raw RMF/Axios/Mongoose/Redis 資料越過 adapter。
  • Queue、emergency、idempotency 與 fail-closed invariants 未改變。
  • 相關 unit/integration/E2E tests 已通過。
  • 若 API/config 有改變,文件與 .env.example 已更新。

15.2 Reviewer 檢查

  • Business rule 是否放在正確模組,而不是依照呼叫來源放置。
  • 新公開 service 是否真的需要加入 feature public。
  • Hospital 是否新增了 repository port 依賴;若有,能否改成 Operations use case。
  • Operations 是否對 CSH/CMP/HIS concrete implementation 產生依賴。
  • Error mapping、HTTP contract 與既有 consumer 相容。
  • 失敗與未知結果是否被正確區分。
  • Async fire-and-forget 是否只用在允許部分失敗的通知。
  • 資料權威與 transaction/atomic update 是否清楚。

15.3 合併與部署

  • 每個 commit 應可建置並可單獨回退。
  • 資料 migration 與純 code refactor 分開提交。
  • Queue schema migration 必須依 task-queue-v2-migration-runbook.md,先停舊 worker,再 migration,再啟動新版。
  • Production image 啟動後,依 deployment-runbook.md 驗證 Mongo、Redis、RMF、Fleet Manager、HMI 與 Hospital integrations。

16. 團隊責任建議

區域主要 owner必要協作者
Operations domain/applicationBackend/Operations teamRMF、Hospital、QA
CSH Hospital workflows/integrationsHospital Integration teamBackend、Hospital API owner、QA
RMF/Robot adaptersRobot Integration teamBackend、Fleet vendor
Product/Hospital HTTP contractAPI ownerConsumer team、QA
Mongo/Redis schema與 migrationBackend/Data ownerSRE、QA
Deployment/configSRE/PlatformBackend、Hospital IT

跨團隊需求開始前,至少先確認:owner、API/port contract、資料權威、失敗語意、idempotency 策略、測試環境與部署順序。

17. 重要檔案索引

檔案用途
src/operations/operations.module.tsOperations feature composition
src/operations/*/*.module.ts各 feature provider/export
src/operations/*/public/index.tsFeature 公開 import 入口
src/operations/ports/跨頂層模組契約
src/operations/workflows/Emergency facade、Queue Worker、Robot Keeper
src/csh-hospital/csh-hospital.module.tsHospital provider 與 workflow composition
src/csh-hospital/workflows/hospital-mission-control.service.ts緊急事件及返回藥局流程
src/csh-hospital/workflows/medicine-cart-task.workflow.tsHMI task progression 與緊急返程協調
src/adapters/adapters.module.tsPort 與 concrete adapter 的 composition
.eslintrc.js靜態 import boundary 規則
test/service/module-boundaries.service.spec.tsModule 結構與 provider graph 驗證
docs/modular-monolith-architecture.md全系統架構摘要
docs/deployment-runbook.md建置、部署與 rollback
docs/task-queue-v2-migration-runbook.mdQueue migration 操作程序

18. 詞彙

  • Feature public entry:對外列出可用 application services 的 public/index.ts。
  • Port:Application 所依賴的抽象能力,以 NestJS token 注入。
  • Adapter:Port 的技術實作,例如 Mongo repository 或 RMF client。
  • Workflow:協調多個 use case 或 feature 的業務流程。
  • Runtime state:RMF/robot 即時狀態,主要由 Redis 保存。
  • Task ownership:機器人目前屬於哪一個 task,由 MongoDB 掌握。
  • Fail closed:外部狀態無法確認時,拒絕派工或自動操作,而不是假設安全。
  • Dispatch reconciliation:RMF 派送結果不明時,依 inbound booking/status 判斷實際結果。
  • Idempotency root:用於判定同一業務任務的穩定 ID;目前為 taskHistoryId。