iT邦幫忙

2026 iThome 鐵人賽

DAY 12
0

摘要
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 單位集合。

為什麼有 unit 還要檢查 system/code?

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
  • 4 份 LAB-UNIT-002 專用 fixture
  • Day 12 journal

先把第六條規則的行為固定下來,會比提早擴張契約載入、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 單位集合

為什麼今天不做完整 UCUM validation?

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 要處理什麼?

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 單位必須使用指定標準系統與允許代碼」。

Day 12 的結果分類

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.orgcode = 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

https://ithelp.ithome.com.tw/upload/images/20260813/20177913VXpEGQcDrC.png

Day 12 從 Day 11 的 44 個測試增加到 50 個測試。

新增的 6 個測試都集中在 LAB-UNIT-002

  • Quantity 具有允許 UCUM system/code:PASS
  • Quantity system 不是 UCUM system:FAIL
  • Quantity code 不在允許集合:FAIL
  • Quantity 缺少 code:FAIL
  • Observation.value[x] 不是 Quantity:NOT_APPLICABLE
  • Bundle 沒有 Observation:NOT_APPLICABLE

常見錯誤 & 排查

  1. LAB-UNIT-002 誤當成完整 UCUM validation

LAB-UNIT-002 不是完整 UCUM validation。

它沒有檢查:

UCUM code 語法是否完整
單位之間是否可以換算
檢驗項目與單位是否臨床合理

它只檢查:

system 是否為 http://unitsofmeasure.org
code 是否在合作契約允許集合

所以文章裡應該稱為「UCUM system/code 契約允許集合檢查」,不要稱為完整 UCUM 驗證。

  1. 以為有 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

這不是矛盾。

它表示資料可讀,但還不符合合作方的機器交換單位政策。

  1. 把非 Quantity value 判成失敗

Observation.value[x] 有很多型別。

如果資料是:

"valueString": "positive"

它不是 LAB-UNIT-002 的適用對象。

這時應該回:

NOT_APPLICABLE

而不是 FAIL

  1. 缺少 code 時回傳空字串或 null

規則結果裡的 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
  • 新增 4 份 LAB-UNIT-002 專用 fixture。
  • 複用既有 observation-value-string.jsonunsupported-resource-in-bundle.json
  • 新增 6 個單元測試。
  • ./mvnw test 通過,測試數從 44 增加到 50。

Day 12 尚未處理:

  • 將交換契約規則接回 BundleParseService
  • 在首頁顯示 RuleResult
  • 契約 YAML/JSON 載入。
  • 契約啟用/停用規則。
  • Quality Gate 的 Passed / Warning / Blocked 判定。
  • 契約 v1.0/v1.1 比較。

目前的規則進度:

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


上一篇
Day11 - 完成 Quantity 可讀單位存在性規則
下一篇
Day13 - 把六條交換規則接成可操作 Quality Gate
系列文
醫療資料通過標準驗證,就真的能交換嗎?——30 天打造 TW Core 資料品質閘門16
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言