iT邦幫忙

2026 iThome 鐵人賽

DAY 8
0

摘要
Day 7 已建立交換契約規則的最小資料模型,並完成第一條 Reference 規則 LAB-REF-001。Day 8 實作第二條 Reference 規則 LAB-REF-002DiagnosticReport.result 必須指向 Bundle 中存在的 Observation

為什麼第二條規則先做 DiagnosticReport.result?

Day 7 處理的是:

Observation.subject → Patient

也就是檢驗結果要能找到它所屬的病人。

Day 8 往檢驗報告的方向前進:

DiagnosticReport.result → Observation

也就是一份檢驗報告列出的檢驗結果,必須真的存在於同一份待驗證 Bundle 裡。

FHIR Resource 的 JSON 格式合法,不代表跨 Resource 關係一定完整。

例如一份 DiagnosticReport 可以寫:

"result": [
  {
    "reference": "Observation/obs-not-in-bundle"
  }
]

這在語法上可能仍是一段可解析的 FHIR JSON。

但對交換情境來說,合作方收到這份 Bundle 後,如果找不到 Observation/obs-not-in-bundle,報告就無法被完整解讀。

LAB-REF-002 要回答的問題是:

這份 DiagnosticReport 說自己包含某筆 Observation,那筆 Observation 在這份 Bundle 裡找得到嗎?

今天的實作範圍

今天新增的範圍只有:

  • LabRef002DiagnosticReportResultRule
  • LabRef002DiagnosticReportResultRuleTests
  • 4 份 LAB-REF-002 專用 fixture
  • Day 8 journal

如果今天同時做契約載入、UI 和規則引擎註冊,很容易變成每一層都只做一半。

現在先讓第二條規則可以被單元測試穩定驗證,會比提早接畫面更重要。

今天完成後,資料鏈從 Day 7 的 Observation -> Patient,擴展成 DiagnosticReport -> Observation -> Patient。

第二條規則要處理什麼?

LAB-REF-002 只看 DiagnosticReport.result.reference

MVP 目前支援兩種 Bundle 內 reference:

reference 形式 範例 Day 8 行為
Resource logical reference Observation/obs-valid-ref 若 Bundle 內有該 Observation,回 PASS
Bundle entry fullUrl urn:uuid:223e4567-e89b-12d3-a456-426614174001 若 fullUrl 對應 Observation,回 PASS

MVP 目前不解析外部 HTTP reference:

reference 形式 範例 Day 8 行為
外部 FHIR Server reference https://example.org/fhir/Observation/external-observation NOT_EVALUATED

原因和 Day 7 一樣。

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

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

比較誠實的狀態仍然是:

NOT_EVALUATED

Day 8 的結果分類

LAB-REF-002 的分類順序如下:

Bundle 沒有 DiagnosticReport → NOT_APPLICABLE
DiagnosticReport 沒有 result → FAIL
result.reference 空白 → FAIL
外部 HTTP reference → NOT_EVALUATED
Bundle 內找得到 Observation → PASS
Bundle 內找不到 Observation → FAIL

這裡有三個容易混淆的地方。

第一,沒有 DiagnosticReport 時不是 FAIL

因為這條規則的適用對象不存在,所以回:

NOT_APPLICABLE

第二,有 DiagnosticReport 但沒有 result 時是 FAIL

因為這份報告沒有列出可追溯的檢驗結果,對本專案的檢驗資料交換情境來說是不完整的。

第三,外部 HTTP reference 是 NOT_EVALUATED

因為系統目前沒有外部查詢能力,不能把沒查過的外部 Observation 當成通過。

測試資料

Day 8 新增或複用以下 fixture:

fixture 用途 LAB-REF-002 預期
valid-internal-reference.json Report result 指向 Bundle 內 Observation logical id PASS
valid-report-result-full-url-reference.json Report result 指向 Bundle entry fullUrl PASS
missing-report-result-reference.json Report result 指向不存在的 Observation FAIL
missing-report-result-field.json DiagnosticReport 沒有 result FAIL
external-report-result-reference.json Report result 指向外部 HTTP Observation NOT_EVALUATED
unsupported-resource-in-bundle.json Bundle 沒有 DiagnosticReport NOT_APPLICABLE

這些 fixture 的目的不是追求案例數量。

它們的目的,是先把 LAB-REF-002 的狀態分類固定下來:

PASS
FAIL
NOT_APPLICABLE
NOT_EVALUATED

後續做情境測試包時,才有穩定的規則行為可以組合。

實作

Day 8 新增:

src/main/java/com/twlab/qualitygate/validation
└─ LabRef002DiagnosticReportResultRule.java

規則仍然實作同一個介面:

public class LabRef002DiagnosticReportResultRule implements ContractRule {

  public static final String RULE_CODE = "LAB-REF-002";

  @Override
  public String ruleCode() {
    return RULE_CODE;
  }

  @Override
  public List<RuleResult> validate(Bundle bundle) {
    // ...
  }
}

這代表 Day 7 建立的 ContractRule 介面可以繼續使用。

第二條規則不需要新的資料模型。

核心邏輯也分成三步:

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

Observation reference 集合包含:

Observation/{id}
Bundle.entry.fullUrl

建立 reference 集合的概念如下:

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

這段和 Day 7 的 Patient reference 集合很像。

但 Day 8 先不抽成共用 helper。

原因是 Reference 類規則還有 LAB-REF-003

等第三條規則完成後,再看三條規則真正重複的是什麼更安全。

接著檢查 DiagnosticReport.result

private List<RuleResult> validateReport(
    DiagnosticReport report,
    Set<String> observationReferences
) {
  if (!report.hasResult()) {
    String path = "DiagnosticReport/" + idOrUnknown(report) + ".result";
    return List.of(fail(path, "N/A", "DiagnosticReport.result is required."));
  }

  List<RuleResult> results = new ArrayList<>();
  List<Reference> reportResults = report.getResult();
  for (int i = 0; i < reportResults.size(); i++) {
    Reference reference = reportResults.get(i);
    String actual = reference.getReference();
    String path = "DiagnosticReport/" + idOrUnknown(report) + ".result[" + i + "].reference";
    results.add(validateReference(path, actual, observationReferences));
  }
  return results;
}

這裡回傳 List<RuleResult>

原因是同一份 DiagnosticReport 可能有多個 result

如果一份報告引用三筆 Observation,規則應該能分別回報三筆結果,而不是把所有 result 混成一個總結。

最後的判斷順序如下:

private RuleResult validateReference(
    String path,
    String actual,
    Set<String> observationReferences
) {
  if (actual == null || actual.isBlank()) {
    return fail(path, "N/A", "DiagnosticReport.result.reference is required.");
  }
  if (isExternalReference(actual)) {
    return notEvaluated(path, actual);
  }
  if (observationReferences.contains(actual)) {
    return pass(path, actual);
  }
  return fail(path, actual, "Observation reference not found in this Bundle.");
}

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

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

六種測試案例

Day 8 新增:

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

測試一:DiagnosticReport 指向 Bundle 內 Observation logical id

assertThat(results.get(0).ruleCode()).isEqualTo("LAB-REF-002");
assertThat(results.get(0).outcome()).isEqualTo(RuleOutcome.PASS);
assertThat(results.get(0).actual()).isEqualTo("Observation/obs-valid-ref");

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

測試二:DiagnosticReport 指向 Bundle entry fullUrl

assertThat(results.get(0).outcome()).isEqualTo(RuleOutcome.PASS);
assertThat(results.get(0).actual()).isEqualTo("urn:uuid:223e4567-e89b-12d3-a456-426614174001");

這代表規則不是只靠 Resource id。

它也能處理 Bundle 內常見的 urn:uuid fullUrl reference。

測試三:DiagnosticReport 指向不存在的 Observation

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

這代表合作方可以得到明確的阻擋原因。

問題不是 JSON 壞掉,而是報告引用了 Bundle 裡不存在的檢驗結果。

測試四:DiagnosticReport 沒有 result

assertThat(results.get(0).outcome()).isEqualTo(RuleOutcome.FAIL);
assertThat(results.get(0).path()).isEqualTo("DiagnosticReport/report-no-result.result");
assertThat(results.get(0).actual()).isEqualTo("N/A");

這代表本專案的檢驗報告情境要求 Report 必須能追溯到 Observation。

測試五:DiagnosticReport 指向外部 HTTP Observation reference

assertThat(results.get(0).outcome()).isEqualTo(RuleOutcome.NOT_EVALUATED);
assertThat(results.get(0).severity()).isEqualTo("warning");
assertThat(results.get(0).actual()).isEqualTo("https://example.org/fhir/Observation/external-observation");

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

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

測試六:Bundle 沒有 DiagnosticReport

assertThat(results.get(0).outcome()).isEqualTo(RuleOutcome.NOT_APPLICABLE);
assertThat(results.get(0).severity()).isEqualTo("information");

這代表規則沒有適用對象時,不會誤判成資料錯誤。

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

自動化驗證

指令:

./mvnw test

測試結果:

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

https://ithelp.ithome.com.tw/upload/images/20260809/20177913hZFlnwKDes.png

Day 8 從 Day 7 的 21 個測試增加到 27 個測試。

新增的 6 個測試都集中在 LAB-REF-002

  • Bundle 內 Observation logical reference:PASS
  • Bundle 內 Observation fullUrl reference:PASS
  • Bundle 內找不到 Observation:FAIL
  • DiagnosticReport 沒有 result:FAIL
  • 外部 HTTP reference:NOT_EVALUATED
  • Bundle 沒有 DiagnosticReport:NOT_APPLICABLE

Day 8 的可展示成果仍然不是 UI。

它是用單元測試把第二條交換契約規則的行為固定下來。

常見錯誤 & 排查

  1. 把沒有 DiagnosticReport 當成 FAIL

LAB-REF-002 的適用對象是 DiagnosticReport

如果 Bundle 裡根本沒有 DiagnosticReport,這條規則沒有可以檢查的對象。

所以回:

NOT_APPLICABLE
  1. 把 DiagnosticReport 沒有 result 當成 NOT_APPLICABLE

這和前一種情況不同。

如果 DiagnosticReport 已經存在,但沒有 result,代表這份檢驗報告沒有列出可追溯的檢驗結果。

在本專案的交換契約裡,這是資料不完整,所以回:

FAIL
  1. 對外部 HTTP reference 回 FAIL

外部 HTTP reference 不一定是錯。

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

所以 Day 8 回:

NOT_EVALUATED
  1. 只靠 Observation/{id},不處理 fullUrl

Bundle 內 reference 不一定只使用 logical id。

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

urn:uuid:223e4567-e89b-12d3-a456-426614174001

所以 Day 8 在建立 Observation reference 集合時,同時保留:

Observation/{id}
entry.fullUrl
  1. 太早抽共用 ReferenceResolver

Day 8 的程式和 Day 7 確實有重複。

但目前只有兩條 Reference 規則,第三條 LAB-REF-003 還沒實作。

先等 LAB-REF-003 完成,再抽共用 reference helper,會比較知道真正要共用的是:

收集 reference
解析 reference
建立 path
分類 external / internal / missing

而不是現在先猜一個抽象。

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

Day 7 的問題是:

Observation 找得到 Patient 嗎?

Day 8 的問題是:

DiagnosticReport 找得到它列出的 Observation 嗎?

這兩條規則加起來,開始形成檢驗資料交換時最基本的可追溯鏈:

Patient
  ↑
Observation
  ↑
DiagnosticReport

如果 DiagnosticReport.result 指向不存在的 Observation,合作方看到的是一份「報告存在,但報告內容缺了一塊」的資料。

這不一定會被 JSON parser 擋下來。

也不應該硬說是 TW Core 官方 Profile 的錯。

它比較適合被歸類為:

Exchange Contract: FAILED

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

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

今天完成了什麼

  • 新增 LabRef002DiagnosticReportResultRule
  • 新增 LabRef002DiagnosticReportResultRuleTests
  • LAB-REF-002 支援 Observation/{id} reference。
  • LAB-REF-002 支援 Bundle.entry.fullUrl reference。
  • LAB-REF-002 對不存在的 Bundle 內 Observation 回 FAIL
  • LAB-REF-002 對缺少 DiagnosticReport.resultFAIL
  • LAB-REF-002 對外部 HTTP reference 回 NOT_EVALUATED
  • LAB-REF-002 對沒有 DiagnosticReport 的 Bundle 回 NOT_APPLICABLE
  • 新增 4 份 LAB-REF-002 專用 fixture。
  • 新增 6 個單元測試。
  • ./mvnw test 通過,測試數從 21 增加到 27。

Day 8 尚未處理:

  • 將交換契約規則接回 BundleParseService
  • 在首頁顯示 RuleResult
  • LAB-REF-003:DiagnosticReport 與 Observation 必須對應同一 Patient。
  • LOINC/UCUM 契約允許集合。
  • 契約 YAML/JSON 載入。
  • 契約啟用/停用規則。
  • Quality Gate 的 Passed / Warning / Blocked 判定。
  • 契約 v1.0/v1.1 比較。

目前的規則進度:

contract-rule
├─ LAB-REF-001  Observation.subject → Patient       完成
├─ LAB-REF-002  DiagnosticReport.result → Observation 完成
├─ LAB-REF-003  Report 與 Observation 對應同一 Patient 未開始
├─ LAB-CODE-001 Observation.code 契約允許 LOINC       未開始
├─ LAB-UNIT-001 Quantity 必須具有可讀 unit             未開始
└─ LAB-UNIT-002 UCUM system / code 契約允許集合         未開始

下一步做 LAB-REF-003

原因是它會同時用到 DiagnosticReport.subjectDiagnosticReport.resultObservation.subject

完成三條 Reference 規則後,再回頭整理共用 Reference helper 更好。

Repository:twcore-data-quality-gate


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

尚未有邦友留言

立即登入留言