摘要
Day 15 已經把契約 v1.0 / v1.1 的代表案例整理成最小情境測試包,證明 Expected / Actual 結果可以穩定重跑。Day 16 把既有的契約版本比較能力接回首頁,讓使用者貼上同一份 Bundle 後,可以直接看到 v1.0 與 v1.1 的 Gate 差異。
Day 15 已經可以在測試裡回答:
同一份 Bundle 在契約 v1.0 與 v1.1 下,Expected / Actual 結果是否穩定?
但畫面仍然只顯示單一版本驗證結果。
今天要補上的問題是:
使用者貼上同一份 Bundle 後,能不能直接看出 v1.0 與 v1.1 的交換結果差在哪裡?
這和交換很有關係。
因為契約升級時,資料可能不是「突然壞掉」,而是「新契約多檢查了一條以前沒檢查的條件」。
如果頁面只顯示最後的 BLOCKED,使用者很難知道:
這是原本就不能交換?
還是升級到 v1.1 後才被擋?
所以 Day 16 的重點是把 Day 14 / Day 15 做好的比較結果,變成可展示的畫面。
今天新增或修改的範圍有:
ParseController
index.html
ParseControllerTests
20260817.md
成果集中在畫面展示層與 controller 測試。
Day 14 已經建立:
ContractComparisonService
它的責任很單純:
public ContractComparisonResult compare(String bundleJson) {
return new ContractComparisonResult(
bundleParseService.parse(bundleJson, ContractVersion.V1_0),
bundleParseService.parse(bundleJson, ContractVersion.V1_1)
);
}
也就是同一份 Bundle 跑兩次:
v1.0 → 啟用舊版規則
v1.1 → 啟用新版規則
今天在 ParseController 裡新增 ContractComparisonService:
private final BundleParseService bundleParseService;
private final ContractComparisonService contractComparisonService;
POST /parse 與 /validate 原本只把單版結果放進 model:
model.addAttribute("result", bundleParseService.parse(input));
今天改成同時放入比較結果:
model.addAttribute("result", bundleParseService.parse(input));
model.addAttribute("comparison", contractComparisonService.compare(input));
這裡沒有改掉原本的單版驗證結果。
原因是目前首頁仍然需要顯示完整的四層驗證:
JSON parse
FHIR R4 parse
FHIR R4 validation
TW Core validation
Exchange contract rule results
新增的 comparison 只補一個視角:
同一份資料在不同契約版本下的 Gate 與失敗規則。
畫面新增一個區塊:
Contract comparison
目前只顯示三欄:
| Contract version | Quality Gate | Failed rule codes |
|---|---|---|
| v1.0 | 依輸入計算 | v1.0 失敗規則 |
| v1.1 | 依輸入計算 | v1.1 失敗規則 |
這個表格刻意保持很小,讓版本比較可以被使用者看見。
Day 14 / Day 15 原本使用的 UCUM system 錯誤案例是:
observation-quantity-wrong-ucum-system.json
這份 fixture 很適合用在單元測試,因為測試裡的 TW Core validation 是 stub 成通過。
在那個情境下,v1.0 與 v1.1 的差異只會來自交換契約規則:
v1.0 → 沒有啟用 LAB-UNIT-002
v1.1 → 啟用 LAB-UNIT-002
但在實際畫面中,系統會執行真的 TW Core Profile validation。
原本那份 fixture 沒有補齊 Patient identifier、gender、birthDate,以及 Observation category、effective[x] 等 TW Core Profile 需要的最小欄位。
結果會變成:
v1.0 → TW Core validation FAILED → BLOCKED
v1.1 → TW Core validation FAILED + LAB-UNIT-002 FAILED → BLOCKED
這樣畫面看起來就會是兩版都 BLOCKED。
它不是契約版本比較壞掉,而是 TW Core 層錯誤先把兩版都擋下來,掩蓋了 v1.1 新增 UCUM 規則造成的差異。
所以 Day 16 新增:
twcore-valid-wrong-ucum-system.json
這份 fixture 先確保 Patient、Observation 與 DiagnosticReport 能通過目前的 TW Core Profile validation,再只把 valueQuantity.system 改成錯誤值。
這樣畫面才能乾淨展示:
v1.0 → PASSED
v1.1 → BLOCKED by LAB-UNIT-002
最重要的展示案例因此改成:
twcore-valid-wrong-ucum-system.json
這份資料的 reference 鏈、LOINC code 與可讀 unit 都是乾淨的。
Patient、Observation 與 DiagnosticReport 也補齊目前 TW Core Profile validation 需要的最小欄位。
唯一問題是 valueQuantity.system 不是 UCUM 官方 URI:
"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 |

這個結果說明:
同一份 Bundle 不是在所有版本都壞掉。
它是在 v1.1 新增 UCUM system/code 要求後才被阻擋。
除了版本差異案例,也需要確認正常資料不會因為新增比較表而看起來像壞掉。
使用:
valid-twcore-contract-bundle.json
預期結果是:
| 契約版本 | Gate 結果 | 失敗規則 |
|---|---|---|
| v1.0 | PASSED | None |
| v1.1 | PASSED | None |

這個案例的用途是保留一個對照組:
不是所有資料升級到 v1.1 都會被阻擋。
只有踩到 v1.1 新增規則的資料才會改變 Gate 結果。
Day 15 的 ContractScenarioCaseTests 已經證明比較服務本身是穩定的。
Day 16 補的是 controller 測試,確認比較結果真的回到畫面:
src/test/java/com/twlab/qualitygate/web/ParseControllerTests.java
新增的第一個測試使用合法 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(containsString("v1.0")))
.andExpect(content().string(containsString("v1.1")))
.andExpect(content().string(containsString("None")));
它確認頁面有顯示:
Contract comparison
v1.0
v1.1
None
新增的第二個測試使用 UCUM system 錯誤案例:
mockMvc.perform(post("/parse")
.param("bundleJson", fixture("twcore-valid-wrong-ucum-system.json")))
.andExpect(status().isOk())
.andExpect(content().string(containsString("Contract comparison")))
.andExpect(content().string(containsString("v1.0")))
.andExpect(content().string(containsString("v1.1")))
.andExpect(content().string(containsString("LAB-UNIT-002")));
它確認使用者在頁面上看得到 v1.1 的失敗規則。
指令:
./mvnw test
測試結果:
Tests run: 59, Failures: 0, Errors: 0, Skipped: 0
BUILD SUCCESS

Day 16 從 57 個測試增加到 59 個測試。
新增的 2 個測試集中在展示入口:
None。LAB-UNIT-002。如果比較表只顯示:
v1.0 → PASSED
v1.1 → BLOCKED
仍然不夠。
因為讀者不知道 v1.1 為什麼阻擋。
所以今天的表格一定要顯示 failed rule codes:
LAB-UNIT-002
這樣才能看出差異來自 UCUM system/code 規則,而不是 reference 或 LOINC 錯誤。
/parse 與 /validate 的 POST flow 接上 ContractComparisonService。Contract comparison 區塊。Quality Gate。./mvnw test 通過,測試數從 57 增加到 59。Day 16 尚未處理:
COMPATIBLE / EXPECTED_BREAKING_CHANGE / UNEXPECTED_REGRESSION 三分類。目前的 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 完成最小版
└─ Scenario test pack
├─ minimal expected / actual table 完成最小版
├─ v1.0 / v1.1 representative cases 完成 4 例
└─ NOT_APPLICABLE scenario fixture 完成 1 例
下一步預計處理:
Expected / Actual 結果整理
補足更多情境案例的覆蓋矩陣
Docker Compose 與 CI quality gate
Repository:twcore-data-quality-gate