摘要
Day 11 已完成第五條交換契約規則LAB-UNIT-001,確認 Quantity 檢驗結果是否具有可讀的valueQuantity.unit。Day 12 接著實作第六條規則LAB-UNIT-002:當Observation.value[x]是Quantity時,valueQuantity.system必須是http://unitsofmeasure.org,而且valueQuantity.code必須存在於合作契約允許的 UCUM 單位集合。
Day 11 的 LAB-UNIT-001 回答的是:
這筆 Quantity 檢驗結果是否具有合作方可直接閱讀的單位?
例如:
"valueQuantity": {
"value": 95,
"unit": "mg/dL"
}
這筆資料對人類來說可以讀。
畫面或報表可以顯示:
95 mg/dL
但交換資料不只需要人類看得懂。
合作方系統也需要能穩定判斷這個單位來自哪個標準系統,以及代碼是否屬於雙方約定允許的集合。
所以一筆比較完整的 Quantity 會長這樣:
"valueQuantity": {
"value": 95,
"unit": "mg/dL",
"system": "http://unitsofmeasure.org",
"code": "mg/dL"
}
其中:
valueQuantity.unit
比較接近顯示與人工閱讀。
而:
valueQuantity.system
valueQuantity.code
比較接近機器判讀與標準化交換。
這就是 LAB-UNIT-002 要處理的問題。
它不是完整 UCUM validation。
它是 UCUM system/code 的交換契約允許集合檢查。
今天新增或修改的範圍有:
LabUnit002ObservationUcumCodeRule
LabUnit002ObservationUcumCodeRuleTests
LAB-UNIT-002 專用 fixture先把第六條規則的行為固定下來,會比提早擴張契約載入、UI 或 Quality Gate 更重要。
今天完成後,六條核心交換規則的進度變成:
Observation.subject → Patient
DiagnosticReport.result → Observation
DiagnosticReport.subject 與 Observation.subject → 同一 Patient
Observation.code → 契約允許 LOINC
Observation.valueQuantity.unit → 可讀單位存在
Observation.valueQuantity.system/code → 契約允許 UCUM 單位集合
LAB-UNIT-002 的名字很容易讓人誤會成完整 UCUM 驗證。
但今天刻意不做完整 UCUM validation。
沒有做:
UCUM 語法解析
單位換算
臨床合理性判斷
依 LOINC 分組的單位政策
terminology server 查詢
今天只做:
system = http://unitsofmeasure.org
code in 合作契約允許集合
目前 MVP 允許的 UCUM code 先固定在規則類別內:
mg/dL
mmol/L
這不是最終契約設計。
它只是先讓第六條核心規則具備可執行、可測試、可解釋的最小行為。
後續加入交換契約 YAML/JSON 後,再把允許集合從程式碼移到契約設定。
LAB-UNIT-002 只看:
Observation.value[x]
Observation.valueQuantity.system
Observation.valueQuantity.code
規則判斷條件如下:
value[x] is Quantity
system = http://unitsofmeasure.org
code in MVP allowed UCUM codes
只要 Observation.value[x] 是 Quantity,而且 system/code 符合契約允許集合,就回 PASS。
如果 Observation.value[x] 是 Quantity,但 system 不是 http://unitsofmeasure.org,就回 FAIL。
如果 Observation.value[x] 是 Quantity,但 code 缺少、空白或不在允許集合,也回 FAIL。
如果 Bundle 沒有 Observation,或 Observation 的 value 不是 Quantity,就回 NOT_APPLICABLE。
目標是展示「資料格式可能成立,但合作方仍然可以要求 Quantity 單位必須使用指定標準系統與允許代碼」。
LAB-UNIT-002 的分類順序如下:
Bundle 沒有 Observation → NOT_APPLICABLE
Observation 沒有 value[x] → NOT_APPLICABLE
Observation.value[x] 不是 Quantity → NOT_APPLICABLE
Observation.valueQuantity.system 不是 http://unitsofmeasure.org → FAIL
Observation.valueQuantity.code 缺少或空白 → FAIL
Observation.valueQuantity.code 不在允許集合 → FAIL
Observation.valueQuantity.system/code 符合契約 → PASS
這裡有三個容易混淆的地方。
第一,有 unit 不代表 LAB-UNIT-002 一定通過。
例如:
"valueQuantity": {
"value": 95,
"unit": "mg/dL",
"system": "http://example.org/local-units",
"code": "mg/dL"
}
這筆資料有可讀單位,所以 LAB-UNIT-001 會通過。
但 system 不是 UCUM system,所以 LAB-UNIT-002 會失敗。
第二,system 正確也不代表一定通過。
例如:
"valueQuantity": {
"value": 95,
"unit": "g/L",
"system": "http://unitsofmeasure.org",
"code": "g/L"
}
這筆資料使用 UCUM system。
但 g/L 不在目前 MVP 合作契約允許集合。
所以仍然回:
FAIL
第三,非 Quantity value 仍然不是失敗。
例如:
"valueString": "positive"
這筆資料不是 LAB-UNIT-002 的適用對象。
所以回:
NOT_APPLICABLE
Day 12 新增或複用以下 fixture:
| fixture | 用途 | LAB-UNIT-002 預期 |
|---|---|---|
valid-ucum-code.json |
system = http://unitsofmeasure.org 且 code = mg/dL |
PASS |
observation-quantity-wrong-ucum-system.json |
Quantity system 不是 UCUM system | FAIL |
observation-quantity-ucum-code-not-allowed.json |
Quantity code 不在 MVP 允許集合 | FAIL |
observation-quantity-without-ucum-code.json |
Quantity 缺少 code | FAIL |
observation-value-string.json |
value[x] 是 valueString,不是 Quantity |
NOT_APPLICABLE |
unsupported-resource-in-bundle.json |
Bundle 沒有 Observation | NOT_APPLICABLE |
這些 fixture 的目的不是追求案例數量。
它們的目的,是先把 LAB-UNIT-002 的最小狀態分類固定下來:
PASS
FAIL
NOT_APPLICABLE
後續加入契約載入或 Quality Gate 時,才有穩定的 UCUM 規則行為可以比對。
Day 12 新增:
src/main/java/com/twlab/qualitygate/validation
└─ LabUnit002ObservationUcumCodeRule.java
規則仍然實作同一個介面:
public class LabUnit002ObservationUcumCodeRule implements ContractRule {
public static final String RULE_CODE = "LAB-UNIT-002";
@Override
public String ruleCode() {
return RULE_CODE;
}
@Override
public List<RuleResult> validate(Bundle bundle) {
// ...
}
}
繼續使用 Day 7 建立的 ContractRule 介面。
第六條規則也不需要新的資料模型。
核心邏輯分成三步:
找出 Bundle 內所有 Observation
↓
確認 Observation.value[x] 是否為 Quantity
↓
確認 valueQuantity.system/code 是否符合契約允許集合
判斷邏輯如下:
private boolean hasAllowedUcumSystemAndCode(Quantity quantity) {
return quantity.hasSystem()
&& UCUM_SYSTEM.equals(quantity.getSystem())
&& quantity.hasCode()
&& !quantity.getCode().isBlank()
&& ALLOWED_UCUM_CODES.contains(quantity.getCode());
}
實際顯示值則收斂成:
private String actualUcumSummary(Quantity quantity) {
String system = quantity.hasSystem() && !quantity.getSystem().isBlank()
? quantity.getSystem()
: "N/A";
String code = quantity.hasCode() && !quantity.getCode().isBlank()
? quantity.getCode()
: "N/A";
return system + "|" + code;
}
這樣缺少 code 時,RuleResult.actual 會是:
http://unitsofmeasure.org|N/A
而不是空字串或 null。
Day 12 新增:
src/test/java/com/twlab/qualitygate/validation/LabUnit002ObservationUcumCodeRuleTests.java
測試一:Quantity 具有允許的 UCUM system/code
assertThat(results.get(0).ruleCode()).isEqualTo("LAB-UNIT-002");
assertThat(results.get(0).outcome()).isEqualTo(RuleOutcome.PASS);
assertThat(results.get(0).path()).isEqualTo("Observation/obs-valid-ucum.valueQuantity.system/code");
assertThat(results.get(0).actual()).isEqualTo("http://unitsofmeasure.org|mg/dL");
這代表最基本的 Quantity 檢驗結果可以通過 UCUM 契約集合檢查。
測試二:Quantity system 不是 UCUM system
assertThat(results.get(0).outcome()).isEqualTo(RuleOutcome.FAIL);
assertThat(results.get(0).severity()).isEqualTo("error");
assertThat(results.get(0).path()).isEqualTo("Observation/obs-wrong-ucum-system.valueQuantity.system/code");
assertThat(results.get(0).actual()).isEqualTo("http://example.org/local-units|mg/dL");
這代表使用本地單位系統時,會被交換契約擋下。
測試三:Quantity code 不在合作契約允許集合
assertThat(results.get(0).outcome()).isEqualTo(RuleOutcome.FAIL);
assertThat(results.get(0).severity()).isEqualTo("error");
assertThat(results.get(0).path()).isEqualTo("Observation/obs-ucum-code-not-allowed.valueQuantity.system/code");
assertThat(results.get(0).actual()).isEqualTo("http://unitsofmeasure.org|g/L");
這代表即使 system 正確,code 不在 MVP 允許集合時仍然不能通過。
測試四:Quantity 缺少 UCUM code
assertThat(results.get(0).outcome()).isEqualTo(RuleOutcome.FAIL);
assertThat(results.get(0).severity()).isEqualTo("error");
assertThat(results.get(0).path()).isEqualTo("Observation/obs-no-ucum-code.valueQuantity.system/code");
assertThat(results.get(0).actual()).isEqualTo("http://unitsofmeasure.org|N/A");
這代表缺少機器可判讀的單位 code 時,會被交換契約擋下。
測試五:Observation.value[x] 不是 Quantity
assertThat(results.get(0).outcome()).isEqualTo(RuleOutcome.NOT_APPLICABLE);
assertThat(results.get(0).path()).isEqualTo("Observation/obs-value-string.value[x]");
assertThat(results.get(0).actual()).isEqualTo("string");
這代表非 Quantity 類型不會被這條規則誤判成錯誤。
測試六:Bundle 沒有 Observation
assertThat(results.get(0).outcome()).isEqualTo(RuleOutcome.NOT_APPLICABLE);
assertThat(results.get(0).severity()).isEqualTo("information");
assertThat(results.get(0).path()).isEqualTo("Bundle.entry");
這代表規則沒有適用對象時,不會誤判成資料錯誤。
這六段 assertion 就是 Day 12 測試的核心。
指令:
./mvnw test
測試結果:
Tests run: 50, Failures: 0, Errors: 0, Skipped: 0
BUILD SUCCESS

Day 12 從 Day 11 的 44 個測試增加到 50 個測試。
新增的 6 個測試都集中在 LAB-UNIT-002:
PASS
FAIL
FAIL
FAIL
NOT_APPLICABLE
NOT_APPLICABLE
LAB-UNIT-002 誤當成完整 UCUM validationLAB-UNIT-002 不是完整 UCUM validation。
它沒有檢查:
UCUM code 語法是否完整
單位之間是否可以換算
檢驗項目與單位是否臨床合理
它只檢查:
system 是否為 http://unitsofmeasure.org
code 是否在合作契約允許集合
所以文章裡應該稱為「UCUM system/code 契約允許集合檢查」,不要稱為完整 UCUM 驗證。
unit 就一定可以通過Day 11 的 LAB-UNIT-001 只檢查:
valueQuantity.unit
Day 12 的 LAB-UNIT-002 檢查:
valueQuantity.system
valueQuantity.code
所以資料可能同時出現:
LAB-UNIT-001 → PASS
LAB-UNIT-002 → FAIL
這不是矛盾。
它表示資料可讀,但還不符合合作方的機器交換單位政策。
Observation.value[x] 有很多型別。
如果資料是:
"valueString": "positive"
它不是 LAB-UNIT-002 的適用對象。
這時應該回:
NOT_APPLICABLE
而不是 FAIL。
規則結果裡的 actual 應該穩定顯示:
http://unitsofmeasure.org|N/A
這樣測試、畫面與文章說明都比較一致。
前五條規則目前回答的是:
資料鏈是否接得起來?
檢驗項目是否屬於合作方允許集合?
檢驗數值是否具有合作方可直接閱讀的單位?
Day 12 的 LAB-UNIT-002 回答的是:
檢驗數值是否使用合作方允許的標準單位 system/code?
也就是說,就算資料長這樣:
DiagnosticReport.result → Observation/obs-1
Observation.subject → Patient/patient-1
Observation.code → http://loinc.org|2345-7
Observation.valueQuantity.unit = mg/dL
還是可能因為:
Observation.valueQuantity.system/code = http://example.org/local-units|mg/dL
而被交換契約擋下。
這不是 JSON 格式錯誤。
也不一定是 TW Core Profile 錯誤。
它比較適合被歸類為:
Exchange Contract: FAILED
這也是資料品質閘門要補上的地方:
標準驗證告訴我們 Resource 結構是否合理。
交換契約規則告訴我們這份資料是否符合合作方接收與機器判讀條件。
LabUnit002ObservationUcumCodeRule。LabUnit002ObservationUcumCodeRuleTests。LAB-UNIT-002 對符合 UCUM system/code 的 Quantity 回 PASS。LAB-UNIT-002 對錯誤 system 回 FAIL。LAB-UNIT-002 對不允許 code 回 FAIL。LAB-UNIT-002 對缺少 code 回 FAIL。LAB-UNIT-002 對非 Quantity value 回 NOT_APPLICABLE。LAB-UNIT-002 對沒有 Observation 的 Bundle 回 NOT_APPLICABLE。LAB-UNIT-002 專用 fixture。observation-value-string.json 與 unsupported-resource-in-bundle.json。./mvnw test 通過,測試數從 44 增加到 50。Day 12 尚未處理:
BundleParseService。RuleResult。目前的規則進度:
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 契約允許集合 完成
下一步預計回到交換契約本身。
原因是目前六條規則已經能證明:
Reference 鏈正確
Observation.code 包含合作方允許 LOINC
Observation.valueQuantity.unit 具有可讀單位
Observation.valueQuantity.system/code 符合合作方允許 UCUM 集合
但這些規則還沒有由契約設定檔統一控制,也還沒有回到畫面與 Quality Gate。
接下來要把規則從單元測試推進到可操作流程:
BundleParseService / rule engine
↓
交換契約 RuleResult
↓
結果頁
↓
Quality Gate
完成這一步後,專案才會從「規則可測試」往「使用者可操作」前進。
Repository:twcore-data-quality-gate