摘要
Day 13 已經把六條交換契約規則接回 Bundle 解析流程與首頁結果頁,使用者可以在同一個畫面看到 JSON、FHIR R4、TW Core、交換契約四層結果,以及最小 Quality Gate。Day 14 切出契約版本比較的第一個可測試骨架:同一份 Bundle 在契約 v1.0 與 v1.1 下,可以因為 v1.1 新增 UCUM system/code 要求而得到不同 Gate 結果。
Day 13 已經可以回答:
這份 Bundle 在目前契約下能不能通過 Quality Gate?
Day 14 開始回答另一個問題:
如果合作方契約升級,同一份 Bundle 的結果會不會改變?
今天的例子是 UCUM system/code。
在 v1.0 中,契約只要求:
Observation.valueQuantity.unit 必須有可讀單位
所以這份資料可以通過:
unit = mg/dL
但在 v1.1 中,契約多要求:
system = http://unitsofmeasure.org
code 必須在允許 UCUM code 集合
同一份資料就會被阻擋:
system = http://example.org/local-units
這就是契約版本比較核心:
不是資料本身突然變壞,而是交換契約變嚴格了。
今天新增或修改的範圍有:
ContractVersion
ContractComparisonResult
ContractComparisonService
BundleParseService
ContractComparisonServiceTests
observation-quantity-wrong-ucum-system.json
今天的成果主要在 service 與 test 層,首頁仍維持 Day 13 的單一版本驗證畫面。
Day 13 的 BundleParseService 會固定執行六條交換契約規則:
LAB-REF-001
LAB-REF-002
LAB-REF-003
LAB-CODE-001
LAB-UNIT-001
LAB-UNIT-002
這很適合單一契約版本。
但如果要展示契約 v1.0 / v1.1 的差異,就需要回答一個問題:
哪個契約版本啟用哪些規則?
Day 14 先用最小 enum 表示這件事:
public enum ContractVersion {
V1_0(Set.of(
LabRef001ObservationSubjectRule.RULE_CODE,
LabRef002DiagnosticReportResultRule.RULE_CODE,
LabRef003ReportObservationPatientRule.RULE_CODE,
LabCode001ObservationLoincRule.RULE_CODE,
LabUnit001ObservationQuantityUnitRule.RULE_CODE
)),
V1_1(Set.of(
LabRef001ObservationSubjectRule.RULE_CODE,
LabRef002DiagnosticReportResultRule.RULE_CODE,
LabRef003ReportObservationPatientRule.RULE_CODE,
LabCode001ObservationLoincRule.RULE_CODE,
LabUnit001ObservationQuantityUnitRule.RULE_CODE,
LabUnit002ObservationUcumCodeRule.RULE_CODE
));
}
這裡的版本差異刻意只有一個:
v1.1 比 v1.0 多啟用 LAB-UNIT-002
也就是 v1.1 開始要求:
Observation.valueQuantity.system 必須是 http://unitsofmeasure.org
Observation.valueQuantity.code 必須在契約允許的 UCUM code 集合中
Day 13 的 parse 方法只有一種行為:
parse(bundleJson) → 執行全部六條規則
Day 14 保留這個預設行為,但把它明確視為最新契約 v1.1:
public ValidationResult parse(String bundleJson) {
return parse(bundleJson, ContractVersion.V1_1);
}
這樣 Day 13 的首頁與既有測試都不用改語意。
原本呼叫 parse(bundleJson) 的地方,仍然會看到六條規則。
接著新增可以指定契約版本的 overload:
public ValidationResult parse(String bundleJson, ContractVersion contractVersion)
真正執行交換規則時,會依照契約版本過濾:
private List<RuleResult> validateContractRules(Bundle bundle, ContractVersion contractVersion) {
return contractRules.stream()
.filter(rule -> contractVersion.enables(rule.ruleCode()))
.flatMap(rule -> rule.validate(bundle).stream())
.toList();
}
這個改法有兩個好處。
第一,Day 13 的單一版本驗證流程不被破壞。
首頁仍然可以把 v1.1 當成目前預設契約。
第二,Day 14 的比較服務可以用同一套解析與 Gate 計算邏輯。
不需要複製 JSON parse、FHIR parse、TW Core validation 或 Quality Gate 的程式。
Day 14 新增一個很薄的比較服務:
public ContractComparisonResult compare(String bundleJson) {
return new ContractComparisonResult(
bundleParseService.parse(bundleJson, ContractVersion.V1_0),
bundleParseService.parse(bundleJson, ContractVersion.V1_1)
);
}
它不做 Change Manifest。
它也不判斷:
COMPATIBLE
EXPECTED_BREAKING_CHANGE
UNEXPECTED_REGRESSION
目前只做最小比較:
同一份 Bundle
↓
用 v1.0 跑一次
↓
用 v1.1 跑一次
↓
回傳兩份 ValidationResult
ContractComparisonResult 也刻意維持很薄:
public record ContractComparisonResult(
ValidationResult v1Result,
ValidationResult v1_1Result
) {
}
也就是說,Day 14 的成果不是完整治理平台。
它只是先把「同一份資料套不同契約版本」這件事接起來。
一開始測試使用:
observation-quantity-wrong-ucum-system.json
但第一次跑測試時發現一個問題:
v1.0 沒有執行 LAB-UNIT-002,仍然是 BLOCKED
原因不是契約版本邏輯錯。
原因是原本這份 fixture 只有 Observation,沒有 Patient 與 DiagnosticReport。
因此即使 v1.0 不檢查 UCUM system/code,它仍然會被 Reference 規則阻擋。
這不是我們今天要展示的差異。
今天需要的是:
除了 LAB-UNIT-002 以外,其他規則都通過
所以 Day 14 把這份 fixture 補成完整三 Resource Bundle:
Patient
Observation
DiagnosticReport
並保留唯一錯誤:
"valueQuantity": {
"value": 95,
"unit": "mg/dL",
"system": "http://example.org/local-units",
"code": "mg/dL"
}
這樣測試語意才乾淨:
v1.0:不啟用 LAB-UNIT-002,所以不因 UCUM system/code 阻擋
v1.1:啟用 LAB-UNIT-002,所以因 UCUM system/code 阻擋
Day 14 新增:
src/test/java/com/twlab/qualitygate/validation/ContractComparisonServiceTests.java
核心測試是:
ContractComparisonResult comparison =
service.compare(fixture("observation-quantity-wrong-ucum-system.json"));
assertThat(comparison.v1Result().gateOutcome()).isEqualTo(GateOutcome.PASSED);
assertThat(comparison.v1Result().contractRuleResults())
.extracting(RuleResult::ruleCode)
.doesNotContain(LabUnit002ObservationUcumCodeRule.RULE_CODE);
assertThat(comparison.v1_1Result().gateOutcome()).isEqualTo(GateOutcome.BLOCKED);
assertThat(comparison.v1_1Result().contractRuleResults())
.anySatisfy(ruleResult -> {
assertThat(ruleResult.ruleCode()).isEqualTo(LabUnit002ObservationUcumCodeRule.RULE_CODE);
assertThat(ruleResult.outcome()).isEqualTo(RuleOutcome.FAIL);
});
這個測試確認三件事。
第一,同一份 Bundle 可以分別用 v1.0 與 v1.1 驗證。
第二,v1.0 的規則結果中不包含:
LAB-UNIT-002
第三,v1.1 的規則結果中包含:
LAB-UNIT-002 → FAIL
所以整體 Gate 會從:
v1.0 → PASSED
v1.1 → BLOCKED
| 契約版本 | 啟用 LAB-UNIT-002 | Gate 結果 | 原因 |
|---|---|---|---|
| v1.0 | 否 | PASSED | 只檢查可讀 unit |
| v1.1 | 是 | BLOCKED | UCUM system 不符 |
Day 14 的重點不是宣稱 v1.1 比 v1.0 好,而是讓系統能重現「契約變嚴格後,同一份資料結果改變」這件事。
指令:
./mvnw test
測試結果:
Tests run: 53, Failures: 0, Errors: 0, Skipped: 0
BUILD SUCCESS

Day 14 從 Day 13 的 52 個測試增加到 53 個測試。
新增的 1 個測試集中在契約版本比較服務:
PASSED。BLOCKED。LAB-UNIT-002。LAB-UNIT-002 且得到 FAIL。這次第一次測試失敗,就是因為 UCUM 錯誤案例本身還缺 Patient 與 DiagnosticReport reference 鏈。
如果同一份資料同時有多個錯誤,就很難證明:
v1.0 / v1.1 差異只來自 LAB-UNIT-002
所以版本比較案例要盡量一次只保留一個差異點。
Day 14 沒有在測試裡手動刪掉某條規則。
而是新增:
ContractVersion.V1_0
ContractVersion.V1_1
再讓 BundleParseService 根據版本決定規則集合。
這樣之後要接 UI、API 或契約檔載入時,才有一個明確的版本入口。
ContractComparisonService 不重新 parse JSON,也不重新計算 Quality Gate。
它只呼叫:
bundleParseService.parse(bundleJson, ContractVersion.V1_0)
bundleParseService.parse(bundleJson, ContractVersion.V1_1)
這可以避免比較流程和單一驗證流程出現不一致。
ContractVersion。ContractVersion.V1_0 啟用五條規則,不含 LAB-UNIT-002。ContractVersion.V1_1 啟用六條規則,包含 LAB-UNIT-002。BundleParseService.parse(String) 保持既有預設行為,預設使用 v1.1。BundleParseService 新增可指定契約版本的 parse(String, ContractVersion)。BundleParseService 依契約版本過濾交換規則。ContractComparisonResult。ContractComparisonService。ContractComparisonServiceTests。observation-quantity-wrong-ucum-system.json,讓它只保留 UCUM system/code 差異。./mvnw test 通過,測試數從 52 增加到 53。Day 14 尚未處理:
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 完成最小版
下一步預計處理:
首頁/API 顯示比較
契約檔載入
Repository:twcore-data-quality-gate