iT邦幫忙

2026 iThome 鐵人賽

DAY 17
0

摘要
Day 15 已經把代表案例整理成 Expected / Actual 測試,Day 16 已經把契約 v1.0 / v1.1 的基本比較結果接回畫面。Day 17 補上「升級後新增阻擋」的最小解讀區,讓使用者看到同一份 Bundle 從 v1.0 PASSED 變成 v1.1 BLOCKED 時,可以直接知道是哪條規則、哪個欄位、實際值是什麼、期待條件是什麼,以及應該怎麼修正。

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

Day 16 已經可以在畫面上回答:

同一份 Bundle 在契約 v1.0 與 v1.1 下,Quality Gate 結果差在哪裡?

例如:

v1.0 → PASSED
v1.1 → BLOCKED by LAB-UNIT-002

但這還不是使用者真正能拿去修資料的答案。
因為 LAB-UNIT-002 只是一個規則代碼。
如果畫面只停在 failed rule codes,使用者還要回頭查:

LAB-UNIT-002 到底檢查哪個欄位?
目前資料寫了什麼?
合作契約期待什麼?
要怎麼改才可能重新通過?

所以 Day 17 要補上的問題是:

契約升級後新增的阻擋,能不能直接轉成可修正的 Expected / Actual 證據?

這和交換很有關係。
資料品質閘門不只要說「不能交換」,還要說清楚:

不能交換的理由在哪個欄位上。

Day 15 的 Expected / Actual 原本主要存在於測試裡。
Day 17 則把同一套概念搬到畫面上,讓它變成使用者可以理解的修正線索。

今天的實作範圍

今天新增或修改的範圍有:

  • ContractComparisonResult
  • index.html
  • ParseControllerTests
  • 20260818.md

成果集中在比較結果的解讀層。
Day 17 的目標只有一個:

把 v1.1 新增失敗規則展開成 path、actual、expected、evidence、suggestion。

ContractComparisonResult 補上最小差異查詢

Day 16 的 ContractComparisonResult 很薄:

public record ContractComparisonResult(
    ValidationResult v1Result,
    ValidationResult v1_1Result
) {
}

它只保存兩版驗證結果。
這樣足夠做比較表,但不適合讓 Thymeleaf 自己處理「v1.1 新增了哪些失敗規則」。
所以 Day 17 在這個 record 裡補兩個查詢方法:

public boolean hasGateOutcomeChange() {
  return v1Result.gateOutcome() != v1_1Result.gateOutcome();
}

第一個方法只回答 Gate 有沒有變:

v1.0 PASSED → v1.1 BLOCKED

第二個方法找出 v1.1 新增的失敗規則:

public List<RuleResult> newlyFailedV11RuleResults() {
  Set<String> v1FailedRuleCodes = v1Result.contractRuleResults().stream()
      .filter(ruleResult -> ruleResult.outcome() == RuleOutcome.FAIL)
      .map(RuleResult::ruleCode)
      .collect(Collectors.toSet());

  return v1_1Result.contractRuleResults().stream()
      .filter(ruleResult -> ruleResult.outcome() == RuleOutcome.FAIL)
      .filter(ruleResult -> !v1FailedRuleCodes.contains(ruleResult.ruleCode()))
      .toList();
}

這裡只做最小集合差異:

v1.1 FAIL 規則 - v1.0 已經 FAIL 的規則

也就是:

只列出升級到 v1.1 後才新增的阻擋。

這不是完整版本治理。
它不判斷這是不是預期 breaking change,也不判斷這是不是非預期回歸。
目前只是把已經存在的 RuleResult 過濾出來,讓畫面可以顯示更完整的修正證據。

畫面新增「升級後新增阻擋」

Day 16 的畫面已經有:

Contract comparison

Day 17 在這個區塊下方新增:

升級後新增阻擋

這個區塊只有在兩個條件同時成立時顯示:

1. v1.0 與 v1.1 的 Quality Gate 結果不同
2. v1.1 有新增的 FAIL 規則

Thymeleaf 條件是:

<div th:if="${comparison.hasGateOutcomeChange()
    && !comparison.newlyFailedV11RuleResults().isEmpty()}">

所以合法資料不會看到這個區塊。
原本 v1.0 就已經失敗的規則,也不會被誤標成「升級後新增阻擋」。
畫面欄位直接沿用 RuleResult

欄位 來源
Rule code RuleResult.ruleCode
Path RuleResult.path
Actual RuleResult.actual
Expected RuleResult.expected
Evidence RuleResult.evidence
Suggestion RuleResult.suggestion

這裡沒有新增新的資料模型。
原因是 Day 8 開始設計 RuleResult 時,就已經把 rule code、path、actual、expected、evidence、suggestion 放進結果物件。
Day 17 做的事情只是把這些欄位放到版本差異旁邊。

UCUM system 錯誤案例的解讀結果

今天使用 Day 16 整理過的乾淨 fixture:

twcore-valid-wrong-ucum-system.json

這份資料的 Patient、Observation 與 DiagnosticReport 已經補齊目前 TW Core Profile validation 需要的最小欄位。
Reference 鏈、LOINC code 與可讀 unit 也都是乾淨的。
唯一刻意留下的問題是 valueQuantity.system

"valueQuantity": {
  "value": 95,
  "unit": "mg/dL",
  "system": "http://example.org/local-units",
  "code": "mg/dL"
}

所以比較表仍然會呈現:

契約版本 Gate 結果 失敗規則
v1.0 PASSED None
v1.1 BLOCKED LAB-UNIT-002

但 Day 17 會再往下補出「升級後新增阻擋」:

Rule code Path Actual Expected
LAB-UNIT-002 Observation/obs-wrong-ucum-system.valueQuantity.system/code http://example.org/local-units|mg/dL Observation.valueQuantity.system must be http://unitsofmeasure.org and code must be allowed by the exchange contract.

Suggestion 也會直接顯示中文修正方向:

請使用合作契約允許的 UCUM 條件:system 必須是 http://unitsofmeasure.org,code 目前允許 mg/dL 或 mmol/L。

https://ithelp.ithome.com.tw/upload/images/20260818/20177913cbdgSF5YFe.png

這個結果說明:

同一份 Bundle 不是單純被 v1.1 擋下。
它是因為 v1.1 開始檢查 UCUM system/code,並且目前 system 寫成 http://example.org/local-units。

把 system 從 http://example.org/local-units 改成 http://unitsofmeasure.org 後,
同一份 Bundle 在 v1.1 也會從 BLOCKED 回到 PASSED。

https://ithelp.ithome.com.tw/upload/images/20260818/2017791377832uwhqy.png

合法 Bundle 不應顯示新增阻擋

Day 17 另一個要確認的是合法 Bundle。
使用:

valid-twcore-contract-bundle.json

預期結果仍然是:

契約版本 Gate 結果 失敗規則
v1.0 PASSED None
v1.1 PASSED None

這個案例不應該出現:

升級後新增阻擋

原因是兩版 Gate 沒有改變。
如果合法資料也顯示升級警告,使用者會誤以為契約 v1.1 本身造成問題。
所以 Day 17 的畫面邏輯必須同時做到兩件事:

有新增阻擋時,清楚顯示修正證據。
沒有新增阻擋時,不額外顯示不必要的資訊。

https://ithelp.ithome.com.tw/upload/images/20260818/2017791377832uwhqy.png

Controller 測試補什麼?

Day 16 的 ParseControllerTests 已經確認比較表會出現在首頁:

Contract comparison
v1.0
v1.1
LAB-UNIT-002

Day 17 補的是畫面解讀證據。
UCUM system 錯誤案例必須在 response HTML 裡看得到:

mockMvc.perform(post("/parse")
    .param("bundleJson", fixture("twcore-valid-wrong-ucum-system.json")))
  .andExpect(status().isOk())
  .andExpect(content().string(containsString("升級後新增阻擋")))
  .andExpect(content().string(containsString("LAB-UNIT-002")))
  .andExpect(content().string(containsString(
      "Observation/obs-wrong-ucum-system.valueQuantity.system/code")))
  .andExpect(content().string(containsString(
      "http://example.org/local-units|mg/dL")))
  .andExpect(content().string(containsString(
      "Observation.valueQuantity.system must be http://unitsofmeasure.org")))
  .andExpect(content().string(containsString(
      "請使用合作契約允許的 UCUM 條件")));

另外合法 Bundle 要確認不會出現新增阻擋區塊:

mockMvc.perform(post("/parse")
    .param("bundleJson", fixture("valid-twcore-contract-bundle.json")))
  .andExpect(status().isOk())
  .andExpect(content().string(containsString("Contract comparison")))
  .andExpect(content().string(not(containsString("升級後新增阻擋"))));

這兩個測試分別保護兩種行為:

  • 有版本差異時,必須顯示可修正證據。
  • 沒有版本差異時,不要顯示誤導性的升級阻擋訊息。

自動化驗證

指令:

./mvnw test

測試結果:

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

https://ithelp.ithome.com.tw/upload/images/20260818/20177913Z0bcn3AQj0.png

Day 17 沒有增加測試總數。
原因是今天修改的是 Day 16 已新增的 controller 測試內容:

  • 合法 Bundle 測試增加「不顯示升級後新增阻擋」的斷言。
  • UCUM system 錯誤測試增加 path、actual、expected、suggestion 的斷言。

也就是說,測試數維持 59,但測試內容變得更精準。

常見錯誤 & 排查

  1. 把所有 v1.1 失敗規則都顯示成新增阻擋

如果 v1.0 本來就因為 LAB-REF-001 失敗,v1.1 也因為 LAB-REF-001 失敗,這不是升級後新增阻擋。

Day 17 的邏輯必須先取出 v1.0 已經失敗的規則:

Set<String> v1FailedRuleCodes = ...

再從 v1.1 的失敗規則裡排除它們。

  1. 合法 Bundle 也顯示「升級後新增阻擋」

這通常代表畫面只檢查 comparison != null,沒有檢查 Gate 是否改變。

正確條件應該包含:

comparison.hasGateOutcomeChange()
  1. 只顯示 Rule code,沒有顯示 Actual / Expected

這樣會退回 Day 16 的狀態。
Day 17 的重點是讓使用者不用再查規則表,就能知道:

Actual:目前資料值
Expected:契約期待條件
Suggestion:中文修正方向

今天完成了什麼

  • ContractComparisonResult 新增 hasGateOutcomeChange()
  • ContractComparisonResult 新增 newlyFailedV11RuleResults()
  • 首頁 Contract comparison 下方新增「升級後新增阻擋」區塊。
  • 新增阻擋區塊顯示 Rule code、Path、Actual、Expected、Evidence、Suggestion。
  • 合法 Bundle 不顯示新增阻擋區塊。
  • UCUM system 錯誤案例顯示 LAB-UNIT-002 的完整修正證據。
  • ParseControllerTests 補強畫面內容斷言。
  • ./mvnw test 通過,測試數維持 59。

Day 17 尚未處理:

  • Change Manifest 與版本變更意圖宣告。
  • COMPATIBLE / EXPECTED_BREAKING_CHANGE / UNEXPECTED_REGRESSION 三分類。
  • JSON Diff 或修正前後差異比較。
  • Docker Compose 與 GitHub Actions CI quality gate。

目前的 MVP 進度:

validation-flow
├─ JSON parse                                  完成
├─ FHIR R4 parse                               完成
├─ FHIR R4 validation                          完成
├─ TW Core validation / safe NOT_EVALUATED      完成
├─ Exchange contract rules
│  ├─ LAB-REF-001                              完成並接回畫面
│  ├─ LAB-REF-002                              完成並接回畫面
│  ├─ LAB-REF-003                              完成並接回畫面
│  ├─ LAB-CODE-001                             完成並接回畫面
│  ├─ LAB-UNIT-001                             完成並接回畫面
│  └─ LAB-UNIT-002                             完成並接回畫面
├─ Quality Gate                                完成最小版
├─ Contract comparison
│  ├─ ContractVersion                          完成最小版
│  ├─ v1.0 / v1.1 rule selection                完成最小版
│  ├─ comparison service test                   完成最小版
│  ├─ homepage comparison display               完成最小版
│  └─ upgrade blocker evidence display          完成最小版
└─ Scenario test pack
   ├─ minimal expected / actual table           完成最小版
   ├─ v1.0 / v1.1 representative cases          完成 4 例
   └─ NOT_APPLICABLE scenario fixture           完成 1 例

下一步預計處理:

如何解讀版本差異
從錯誤案例到修正版資料
Docker Compose 與 CI quality gate

Repository:twcore-data-quality-gate


上一篇
Day16 - 契約版本比較接回畫面
系列文
醫療資料通過標準驗證,就真的能交換嗎?——30 天打造 TW Core 資料品質閘門17
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言