iT邦幫忙

2026 iThome 鐵人賽

DAY 7
0

摘要
Day 6 已確認 TW Core Profile validation chain 可以實際執行,並且能把 TW Core Profile issues 和 FHIR R4 OperationOutcome 分開顯示。Day 7 建立交換契約規則的最小資料模型,並實作第一條 Reference 規則 LAB-REF-001 的測試骨架。

為什麼 TW Core validation 後,還要做交換契約規則?

Day 6 已經回答:

系統能不能用 TW Core Profile 驗證 Bundle 內的 Patient、Observation、DiagnosticReport?

Day 7 開始處理下一個問題:

就算資料能被標準 Profile 驗證,合作方真的能接受這份檢驗資料嗎?

TW Core Profile validation 檢查的是 Resource 是否符合指定 Profile。

交換契約規則檢查的是某個交換情境下,合作方需要的資料關係與阻擋條件。

例如本專案的第一條交換契約規則:

LAB-REF-001:Observation.subject 必須指向 Bundle 中存在的 Patient。

這不是要取代 TW Core,也不是把專案規則包裝成官方要求。

它回答的是更接近交換流程的問題:

這筆 Observation 說自己屬於某個 Patient,那個 Patient 在這份待驗證 Bundle 裡找得到嗎?

今天的實作範圍

Day 7 只做 Reference 規則入口。

今天新增的範圍只有:

  • RuleOutcome
  • RuleResult
  • ContractRule
  • LabRef001ObservationSubjectRule
  • LabRef001ObservationSubjectRuleTests

目標是先把交換規則的資料結構與第一條規則行為固定下來,避免直接把六條規則、UI 和契約設定一起做,導致每一層都只完成一半。

第一條規則要處理什麼?

LAB-REF-001 只看 Observation.subject.reference

MVP 目前支援兩種 Bundle 內 reference:

reference 形式 範例 Day 7 行為
Resource logical reference Patient/patient-valid-ref 若 Bundle 內有該 Patient,回 PASS
Bundle entry fullUrl urn:uuid:423e4567-e89b-12d3-a456-426614174010 若 fullUrl 對應 Patient,回 PASS

MVP 目前不解析外部 HTTP reference:

reference 形式 範例 Day 7 行為
外部 FHIR Server reference https://example.org/fhir/Patient/external-patient NOT_EVALUATED

原因是外部 reference 需要連到外部 FHIR Server 才能確認 Patient 是否存在。

Day 7 還沒有外部查詢能力,所以不能說通過,也不能說資料錯誤。

比較誠實的狀態是:

NOT_EVALUATED

這延續前幾天的原則:

沒有真的檢查,就不能標示 PASS。

測試資料

Day 7 沿用 Day 5 先準備好的三份 fixture:

fixture 用途 LAB-REF-001 預期
valid-internal-reference.json Observation 指向 Bundle 內存在的 Patient PASS
missing-internal-reference.json Observation 指向不存在於 Bundle 的 Patient FAIL
external-http-reference.json Observation 指向外部 HTTP Patient reference NOT_EVALUATED

這三份資料在 Day 5 只是 reference exploration fixture。

Day 7 開始,它們正式成為第一條交換契約規則的測試素材。

實作

Day 7 新增最小交換契約規則資料模型:

src/main/java/com/twlab/qualitygate/validation
├─ RuleOutcome.java
├─ RuleResult.java
├─ ContractRule.java
└─ LabRef001ObservationSubjectRule.java
  1. 建立 RuleOutcome
public enum RuleOutcome {
  PASS,
  FAIL,
  NOT_APPLICABLE,
  NOT_EVALUATED
}

這裡先不用 ParseStatus

原因是 ParseStatus 是前面 JSON、FHIR R4、TW Core 這些驗證層的狀態;RuleOutcome 是單條交換契約規則的結果。

兩者相似,但語意不同。

  1. 建立 RuleResult
public record RuleResult(
    String ruleCode,
    RuleOutcome outcome,
    String severity,
    String path,
    String actual,
    String expected,
    String evidence,
    String suggestion
) {}

這個欄位設計對應 30 天計畫裡的規則輸出需求:

欄位 意義
ruleCode 規則編號,例如 LAB-REF-001
outcome PASS / FAIL / NOT_APPLICABLE / NOT_EVALUATED
severity 未來 Quality Gate 可用的嚴重度
path 問題位置
actual 實際 reference
expected 預期條件
evidence 判定依據
suggestion 中文修正方向
  1. 建立 ContractRule
public interface ContractRule {

  String ruleCode();

  List<RuleResult> validate(Bundle bundle);
}

這裡回傳 List<RuleResult>,不是單一 RuleResult

原因是同一個 Bundle 可能有多個 Observation。

如果未來一份 Bundle 有三筆 Observation,LAB-REF-001 就應該能分別回報三筆結果,而不是把所有 Observation 混成一個總結。

  1. 實作 LabRef001ObservationSubjectRule

核心邏輯分成三步:

讀取 Bundle 內所有 Patient
        ↓
建立可接受的 Patient reference 集合
        ↓
逐一檢查 Observation.subject.reference

Patient reference 集合包含:

Patient/{id}
Bundle.entry.fullUrl

如果 Observation 沒有 subject.reference,回 FAIL

如果 reference 是 http://https://,回 NOT_EVALUATED

如果 reference 可以在 Bundle 內找到 Patient,回 PASS

如果 reference 是 Bundle 內形式,但找不到 Patient,回 FAIL

以下是為了文章閱讀而簡化的核心片段,省略 path 建立與 RuleResult helper 細節。

核心程式碼可以分成兩段來看:

private Set<String> collectPatientReferences(Bundle bundle) {
  Set<String> patientReferences = new HashSet<>();
  for (Bundle.BundleEntryComponent entry : bundle.getEntry()) {
    Resource resource = entry.getResource();
    if (resource instanceof Patient patient) {
      String id = patient.getIdElement().getIdPart();
      if (id != null && !id.isBlank()) {
        patientReferences.add("Patient/" + id);
      }
      String fullUrl = entry.getFullUrl();
      if (fullUrl != null && !fullUrl.isBlank()) {
        patientReferences.add(fullUrl);
      }
    }
  }
  return patientReferences;
}

這段先把 Bundle 內所有 Patient 轉成可比對的 reference 集合。

同一個 Patient 會同時保留兩種定位方式:

Patient/{id}
entry.fullUrl

接著逐一檢查 Observation:

private RuleResult validateObservation(
    Observation observation,
    Set<String> patientReferences
) {
  String actual = observation.hasSubject()
      ? observation.getSubject().getReference()
      : null;

  if (actual == null || actual.isBlank()) {
    return fail(path, "N/A", "Observation.subject.reference is required.");
  }
  if (isExternalReference(actual)) {
    return notEvaluated(path, actual);
  }
  if (patientReferences.contains(actual)) {
    return pass(path, actual);
  }
  return fail(path, actual, "Patient reference not found in this Bundle.");
}

這段的重點不是語法,而是分類順序:

沒有 subject.reference → FAIL
外部 HTTP reference → NOT_EVALUATED
Bundle 內找得到 Patient → PASS
Bundle 內找不到 Patient → FAIL

三種測試案例

Day 7 新增:

src/test/java/com/twlab/qualitygate/validation/LabRef001ObservationSubjectRuleTests.java

測試一:Observation 指向 Bundle 內 Patient

assertThat(results.get(0).outcome()).isEqualTo(RuleOutcome.PASS);
assertThat(results.get(0).actual()).isEqualTo("Patient/patient-valid-ref");

這代表最基本的內部 reference 可以通過。

測試二:Observation 指向不存在的 Patient

assertThat(results.get(0).outcome()).isEqualTo(RuleOutcome.FAIL);
assertThat(results.get(0).severity()).isEqualTo("error");
assertThat(results.get(0).actual()).isEqualTo("Patient/patient-not-in-bundle");

這代表合作方可以得到明確的阻擋原因,而不是只看到「FHIR 格式合法」。

測試三:Observation 指向外部 HTTP Patient reference

assertThat(results.get(0).outcome()).isEqualTo(RuleOutcome.NOT_EVALUATED);
assertThat(results.get(0).severity()).isEqualTo("warning");

這代表系統承認 MVP 還不支援外部查詢。

它不把外部 reference 當成通過,也不直接說資料錯。

這三段 assertion 就是 Day 7 測試的核心,不需要再另外截測試檔畫面。

自動化驗證

指令:

./mvnw test

測試結果:

Tests run: 21, Failures: 0, Errors: 0, Skipped: 0
BUILD SUCCESS

https://ithelp.ithome.com.tw/upload/images/20260808/20177913sFGTUdAJOy.png

Day 7 從原本 18 個測試增加到 21 個測試。

新增的 3 個測試都集中在 LAB-REF-001

  • Bundle 內 Patient reference:PASS
  • Bundle 內找不到 Patient:FAIL
  • 外部 HTTP reference:NOT_EVALUATED

Day 7 的可展示成果不是 UI,而是先用單元測試把第一條交換契約規則的行為固定下來。

常見錯誤 & 排查

  1. 把 Reference rule 混進 TW Core Profile validation

LAB-REF-001 是交換契約規則,不是 TW Core Profile validator 的一部分。

Day 7 沒有修改:

HapiTwCoreProfileValidator
TwCoreValidationService
TwCoreValidationResult

這樣可以維持分層:

FHIR R4 / TW Core Profile / Exchange Contract

各層回答不同問題。

  1. 對外部 HTTP reference 回 FAIL

外部 HTTP reference 不一定是錯。

它只是超出 MVP 目前能力,因為系統沒有去外部 FHIR Server 查詢。

所以 Day 7 回:

NOT_EVALUATED
  1. 只靠 Resource id,不處理 fullUrl

Bundle 內 reference 不一定只使用 Patient/{id}

後續也可能透過 Bundle.entry.fullUrl 對應,例如 urn:uuid:...

所以 Day 7 在建立 Patient reference 集合時,同時保留:

Patient/{id}
entry.fullUrl
  1. 直接把規則結果接到 UI

Day 7 先不接 UI。

原因是現在還只有第一條規則,如果太早設計畫面,後面 LAB-REF-002LAB-REF-003、LOINC、UCUM 都可能讓畫面模型重做。

先用單元測試固定規則行為,比較符合三小時內的停止線。

這和「能不能交換」有什麼關係?

Day 6 看到的是:

FHIR R4 validation: PASSED
TW Core validation: FAILED

Day 7 開始準備下一種情境:

FHIR R4 validation: PASSED
TW Core validation: PASSED 或 FAILED
Exchange Contract: FAILED

也就是說,即使前面的標準驗證層可以執行,交換情境仍然可能因為 reference 關係不完整而被阻擋。

LAB-REF-001 是第一個例子。

Observation 不能只寫自己是檢驗結果,還要能指向這份 Bundle 裡可解析的 Patient。

這也是資料品質閘門要補上的地方:

標準驗證告訴我們 Resource 結構是否合理。
交換契約規則告訴我們這份資料是否符合合作方接收條件。

今天完成了什麼

  • 新增 RuleOutcome
  • 新增 RuleResult
  • 新增 ContractRule
  • 新增 LabRef001ObservationSubjectRule
  • LAB-REF-001 支援 Patient/{id} reference。
  • LAB-REF-001 支援 Bundle.entry.fullUrl reference。
  • LAB-REF-001 對不存在的 Bundle 內 Patient 回 FAIL
  • LAB-REF-001 對外部 HTTP reference 回 NOT_EVALUATED
  • 沿用三份 reference fixture 作為正式測試素材。
  • 新增 3 個單元測試。
  • ./mvnw test 通過,測試數從 18 增加到 21。

Day 7 尚未處理:

  • 將交換契約規則接回 BundleParseService
  • 在首頁顯示 RuleResult
  • LAB-REF-002:DiagnosticReport.result 必須指向 Bundle 中存在的 Observation。
  • LAB-REF-003:DiagnosticReport 與 Observation 必須對應同一 Patient。
  • LOINC/UCUM 契約允許集合。
  • Quality Gate 的 Passed / Warning / Blocked 判定。
  • 契約 v1.0/v1.1 比較。

後續分層預計會變成:

quality-gate
├─ parser
├─ fhir-r4-validator
├─ resource-inventory
├─ tw-core-package-probe
├─ tw-core-validator
├─ contract-rule
│  ├─ LAB-REF-001
│  ├─ LAB-REF-002
│  └─ LAB-REF-003
└─ report

Repository:twcore-data-quality-gate


上一篇
Day6 - 第一次執行 TW Core Profile Validation
下一篇
Day8 - 完成第二條交換契約 Reference 規則
系列文
醫療資料通過標準驗證,就真的能交換嗎?——30 天打造 TW Core 資料品質閘門10
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言