摘要
Day 10 已完成第四條交換契約規則LAB-CODE-001,確認Observation.code是否包含合作方允許的 LOINC coding。Day 11 接著實作第五條規則LAB-UNIT-001:當Observation.value[x]是Quantity時,valueQuantity.unit必須存在,讓檢驗數值不只機器可解析,也能被合作方系統與人工流程直接閱讀。
Day 10 的 LOINC 規則回答的是:
這筆 Observation 是不是合作方允許接收的檢驗項目?
例如:
Observation.code.coding = http://loinc.org|2345-7
但就算檢驗項目是允許的,檢驗結果本身仍然可能缺少必要的單位資訊。
例如一筆血糖結果可能長這樣:
"valueQuantity": {
"value": 95,
"system": "http://unitsofmeasure.org",
"code": "mg/dL"
}
這筆資料有 value,也有 UCUM 相關的 system 與 code。
但缺少人類可讀的:
valueQuantity.unit
如果合作方的畫面或報表直接顯示檢驗結果,缺少 unit 會讓結果變成:
95
而不是:
95 mg/dL
FHIR parser 可能可以解析這筆資料。
TW Core Profile 也不一定會把這個合作方顯示需求視為交換阻擋條件。
但從交換情境來看,合作方會問的是:
這筆 Quantity 檢驗結果是否具有可直接閱讀的單位?
這就是 LAB-UNIT-001 要處理的問題。
它不是完整 UCUM validation。
它是 Quantity 可讀單位存在性檢查。
今天新增或修改的範圍只有:
LabUnit001ObservationQuantityUnitRule
LabUnit001ObservationQuantityUnitRuleTests
LAB-UNIT-001 專用 fixture先把第五條規則的行為固定下來,會比提早擴張 UI 或契約版本功能更重要。
今天完成後,六條核心交換規則的進度變成:
Observation.subject → Patient
DiagnosticReport.result → Observation
DiagnosticReport.subject 與 Observation.subject → 同一 Patient
Observation.code → 契約允許 LOINC
Observation.valueQuantity.unit → 可讀單位存在
剩下最後一條:
Observation.valueQuantity.system/code → 契約允許 UCUM 單位集合
Quantity 裡常見的單位欄位有三個:
valueQuantity.unit
valueQuantity.system
valueQuantity.code
它們處理的是不同層次。
unit 比較接近顯示與人工閱讀需求:
"unit": "mg/dL"
system 與 code 比較接近機器判讀與標準化交換需求:
"system": "http://unitsofmeasure.org",
"code": "mg/dL"
Day 11 先做 LAB-UNIT-001,只確認:
當 value[x] 是 Quantity 時,unit 必須存在
Day 12 再做 LAB-UNIT-002,確認:
system = http://unitsofmeasure.org
code in 合作契約允許的 UCUM 單位集合
這樣拆開的好處是,錯誤訊息會比較清楚。
缺少可讀單位時,使用者看到的是:
請補上合作方可讀的檢驗單位,例如 mg/dL。
UCUM system 或 code 不符合契約時,Day 12 再回另一個更精準的錯誤。
LAB-UNIT-001 只看:
Observation.value[x]
Observation.valueQuantity.unit
規則判斷條件如下:
value[x] is Quantity
unit exists and is not blank
只要 Observation.value[x] 是 Quantity,而且 valueQuantity.unit 有值,就回 PASS。
如果 Observation.value[x] 是 Quantity,但 unit 缺少或空白,就回 FAIL。
如果 Bundle 沒有 Observation,或 Observation 的 value 不是 Quantity,就回 NOT_APPLICABLE。
目標是展示「交換契約可以補上 Profile 驗證之外的合作方顯示需求」。
LAB-UNIT-001 的分類順序如下:
Bundle 沒有 Observation → NOT_APPLICABLE
Observation 沒有 value[x] → NOT_APPLICABLE
Observation.value[x] 不是 Quantity → NOT_APPLICABLE
Observation.valueQuantity.unit 缺少或空白 → FAIL
Observation.valueQuantity.unit 有值 → PASS
這裡有三個容易混淆的地方。
第一,沒有 Observation 時不是 FAIL。
因為這條規則的適用對象不存在,所以回:
NOT_APPLICABLE
第二,Observation.value[x] 不是 Quantity 時也不是 FAIL。
例如:
"valueString": "positive"
這筆資料不一定錯。
它只是超出 LAB-UNIT-001 的檢查範圍。
所以回:
NOT_APPLICABLE
第三,Day 11 沒有檢查 system 與 code。
例如:
"valueQuantity": {
"value": 95,
"unit": "mg/dL"
}
在 LAB-UNIT-001 會先通過。
至於是否具有:
system = http://unitsofmeasure.org
code = mg/dL
那是 LAB-UNIT-002 的責任。
Day 11 新增或複用以下 fixture:
| fixture | 用途 | LAB-UNIT-001 預期 |
|---|---|---|
valid-minimal-lab-bundle.json |
valueQuantity.unit = mg/dL |
PASS |
observation-quantity-without-unit.json |
有 valueQuantity,但缺少 unit |
FAIL |
observation-value-string.json |
value[x] 是 valueString,不是 Quantity |
NOT_APPLICABLE |
unsupported-resource-in-bundle.json |
Bundle 沒有 Observation | NOT_APPLICABLE |
這些 fixture 的目的不是追求案例數量。
它們的目的,是先把 LAB-UNIT-001 的最小狀態分類固定下來:
PASS
FAIL
NOT_APPLICABLE
後續加入 LAB-UNIT-002 時,才有穩定的 Quantity unit 行為可以比對。
Day 11 新增:
src/main/java/com/twlab/qualitygate/validation
└─ LabUnit001ObservationQuantityUnitRule.java
規則仍然實作同一個介面:
public class LabUnit001ObservationQuantityUnitRule implements ContractRule {
public static final String RULE_CODE = "LAB-UNIT-001";
@Override
public String ruleCode() {
return RULE_CODE;
}
@Override
public List<RuleResult> validate(Bundle bundle) {
// ...
}
}
代表 Day 7 建立的 ContractRule 介面仍然可以繼續使用。
第五條規則也不需要新的資料模型。
核心邏輯分成三步:
找出 Bundle 內所有 Observation
↓
確認 Observation.value[x] 是否為 Quantity
↓
確認 valueQuantity.unit 是否存在且不是空白
判斷邏輯如下:
private boolean hasReadableUnit(Quantity quantity) {
return quantity.hasUnit() && !quantity.getUnit().isBlank();
}
實際顯示值則收斂成:
private String actualUnit(Quantity quantity) {
return hasReadableUnit(quantity) ? quantity.getUnit() : "N/A";
}
這樣缺少 unit 時,RuleResult.actual 會是:
N/A
而不是空字串或 null。
Day 11 新增:
src/test/java/com/twlab/qualitygate/validation/LabUnit001ObservationQuantityUnitRuleTests.java
測試一:Quantity 有可讀 unit
assertThat(results.get(0).ruleCode()).isEqualTo("LAB-UNIT-001");
assertThat(results.get(0).outcome()).isEqualTo(RuleOutcome.PASS);
assertThat(results.get(0).path()).isEqualTo("Observation/obs-1.valueQuantity.unit");
assertThat(results.get(0).actual()).isEqualTo("mg/dL");
這代表最基本的 Quantity 檢驗結果可以通過。
測試二:Quantity 缺少 unit
assertThat(results.get(0).outcome()).isEqualTo(RuleOutcome.FAIL);
assertThat(results.get(0).severity()).isEqualTo("error");
assertThat(results.get(0).path()).isEqualTo("Observation/obs-quantity-no-unit.valueQuantity.unit");
assertThat(results.get(0).actual()).isEqualTo("N/A");
這代表有檢驗數值但缺少可讀單位時,會被交換契約擋下。
測試三: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 11 測試的核心。
指令:
./mvnw test
測試結果:
Tests run: 44, Failures: 0, Errors: 0, Skipped: 0
BUILD SUCCESS

Day 11 從 Day 10 的 40 個測試增加到 44 個測試。
新增的 4 個測試都集中在 LAB-UNIT-001:
PASS
FAIL
NOT_APPLICABLE
NOT_APPLICABLE
LAB-UNIT-001 誤當成完整 UCUM validationLAB-UNIT-001 不是完整 UCUM validation。
它沒有檢查:
system = http://unitsofmeasure.org
code 是否在允許集合
它只檢查:
valueQuantity.unit 是否存在
所以文章裡應該稱為「可讀單位存在性檢查」,不要稱為完整 UCUM 驗證。
Observation.value[x] 有很多型別。
如果資料是:
"valueString": "positive"
它不是 LAB-UNIT-001 的適用對象。
這時應該回:
NOT_APPLICABLE
而不是 FAIL。
system/code,忘記 unit
Day 12 會處理 system/code。
但 Day 11 的重點是可讀單位:
valueQuantity.unit
如果只看 system/code,就無法展示「機器標準化」與「人工可讀」是兩個不同層次。
規則結果裡的 actual 應該穩定顯示:
N/A
這樣測試、畫面與文章說明都比較一致。
前四條規則目前回答的是:
資料鏈是否接得起來?
檢驗項目是否屬於合作方允許集合?
Day 11 的 LAB-UNIT-001 回答的是:
檢驗數值是否具有合作方可直接閱讀的單位?
也就是說,就算資料長這樣:
DiagnosticReport.result → Observation/obs-1
Observation.subject → Patient/patient-1
Observation.code → http://loinc.org|2345-7
還是可能因為:
Observation.valueQuantity.unit = N/A
而被交換契約擋下。
這不是 JSON 格式錯誤。
也不一定是 TW Core Profile 錯誤。
它比較適合被歸類為:
Exchange Contract: FAILED
這也是資料品質閘門要補上的地方:
標準驗證告訴我們 Resource 結構是否合理。
交換契約規則告訴我們這份資料是否符合合作方接收與閱讀條件。
LabUnit001ObservationQuantityUnitRule。LabUnit001ObservationQuantityUnitRuleTests。LAB-UNIT-001 對 Quantity 有 unit 回 PASS。LAB-UNIT-001 對 Quantity 缺少 unit 回 FAIL。LAB-UNIT-001 對非 Quantity value 回 NOT_APPLICABLE。LAB-UNIT-001 對沒有 Observation 的 Bundle 回 NOT_APPLICABLE。LAB-UNIT-001 專用 fixture。valid-minimal-lab-bundle.json 與 unsupported-resource-in-bundle.json。./mvnw test 通過,測試數從 40 增加到 44。Day 11 尚未處理:
LAB-UNIT-002 UCUM system/code 契約允許集合。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 契約允許集合 未開始
下一步預計進入 LAB-UNIT-002。
原因是目前已經能證明:
Reference 鏈正確
Observation.code 包含合作方允許 LOINC
Observation.valueQuantity.unit 具有可讀單位
但還不能判斷 Quantity 類型的檢驗單位是否使用合作契約允許的 UCUM system 與 code。
完成 LAB-UNIT-002 後,六條核心交換規則就會先具備完整骨架。
Repository:twcore-data-quality-gate