摘要
Day 9 已完成三條 Reference 規則,確認Observation.subject、DiagnosticReport.result,以及DiagnosticReport.subject與Observation.subject的同病人關係。Day 10 先整理這三條規則重複的 Bundle 內 reference 解析邏輯,再實作第四條交換契約規則LAB-CODE-001:Observation.code必須包含合作方契約允許的 LOINC coding。
前三條 Reference 規則處理的是資料鏈是否接得起來:
Patient
↑
Observation
↑
DiagnosticReport
也就是先確認:
Observation.subject → Patient
DiagnosticReport.result → Observation
DiagnosticReport.subject 與 Observation.subject → 同一 Patient
但 Reference 都正確,仍然不代表合作方一定能接收這筆檢驗資料。
例如一筆 Observation 可以正確指向 Patient,也可以被 DiagnosticReport 正確引用,但它的檢驗代碼可能不是合作契約允許的代碼:
Observation.code.coding.system = http://loinc.org
Observation.code.coding.code = 9999-9
FHIR parser 可能可以解析這筆資料。
TW Core Profile 也不一定會知道某個合作方只接受哪些檢驗項目。
但從交換情境來看,合作方會問的是:
這筆 Observation.code 是否屬於本次交換契約允許的 LOINC 集合?
這就是 LAB-CODE-001 要處理的問題。
它不是完整 terminology validation。
它是交換契約允許集合檢查。
今天新增或修改的範圍只有:
BundleReferenceIndex
LabRef001ObservationSubjectRule
LabRef002DiagnosticReportResultRule
LabRef003ReportObservationPatientRule
LabCode001ObservationLoincRule
LabCode001ObservationLoincRuleTests
LAB-CODE-001 專用 fixture先讓第四條規則可以被單元測試穩定驗證,會比提早接畫面更重要。
今天完成後,六條核心交換規則的進度變成:
Observation.subject → Patient
DiagnosticReport.result → Observation
DiagnosticReport.subject 與 Observation.subject → 同一 Patient
Observation.code → 契約允許 LOINC
Day 7、Day 8、Day 9 的 Reference 規則都需要處理同一批 MVP 邊界:
Resource logical reference,例如 Patient/patient-1
Bundle.entry.fullUrl,例如 urn:uuid:...
外部 HTTP reference,例如 https://example.org/fhir/Patient/external
沒有 id 時的 UNKNOWN path 顯示
Day 9 之前先不抽 helper,是因為三條規則還沒全部完成。
第三條規則完成後,才比較清楚真正共用的是:
收集 Resource logical reference
收集 Bundle.entry.fullUrl
辨識 external HTTP reference
建立 Patient fullUrl 與 logical id alias
建立穩定 path 顯示
所以 Day 10 新增:
src/main/java/com/twlab/qualitygate/validation
└─ BundleReferenceIndex.java
這個 helper 是 package-private。
它不是新的 public API,也不改 ContractRule、RuleResult 或 RuleOutcome。
它只是把三條 Reference 規則裡重複的 Bundle reference indexing 收斂到同一個地方。
整理後的 helper 角色大致如下:
final class BundleReferenceIndex {
static Set<String> referencesTo(Bundle bundle, Class<? extends Resource> resourceType, String resourceName) {
// 建立 Resource/{id} 與 Bundle.entry.fullUrl reference 集合
}
static Map<String, Observation> observationsByReference(Bundle bundle) {
// 讓 DiagnosticReport.result 可以找到 Bundle 內 Observation
}
static Map<String, String> patientReferenceAliases(Bundle bundle) {
// 讓 Patient/{id} 與同一個 Patient entry.fullUrl 對應到同一個 canonical reference
}
static boolean isExternalReference(String reference) {
return reference != null && (reference.startsWith("http://") || reference.startsWith("https://"));
}
}
LAB-CODE-001 只看:
Observation.code.coding[*].system
Observation.code.coding[*].code
MVP 目前固定允許兩個 LOINC code:
| LOINC code | 用途 |
|---|---|
2345-7 |
Glucose 類示範案例 |
718-7 |
Hemoglobin 類示範案例 |
規則判斷條件如下:
system = http://loinc.org
code in [2345-7, 718-7]
只要 Observation.code.coding[*] 任一筆符合條件,就回 PASS。
如果 Observation 存在,但沒有任何一筆 coding 符合條件,就回 FAIL。
目標是展示「合作方契約允許集合」這一層,就足以說明格式驗證之外還有交換契約規則。
LAB-CODE-001 的分類順序如下:
Bundle 沒有 Observation → NOT_APPLICABLE
Observation.code 沒有 coding → FAIL
Observation.code.coding 缺 code → FAIL
Observation.code.coding 不是 http://loinc.org → FAIL
Observation.code.coding 是 LOINC 但不在允許集合 → FAIL
Observation.code.coding 包含允許 LOINC → PASS
這裡有三個容易混淆的地方。
第一,沒有 Observation 時不是 FAIL。
因為這條規則的適用對象不存在,所以回:
NOT_APPLICABLE
第二,有 Observation 但沒有可用的 code.coding 時是 FAIL。
因為 LAB-CODE-001 要確認檢驗代碼是否屬於合作契約允許集合。
如果 Observation 存在卻沒有 coding,合作方無法用契約集合判斷這是哪一個檢驗項目。
第三,Day 10 沒有把不允許的 LOINC 回成 NOT_EVALUATED。
因為目前規則已經有固定允許集合:
2345-7
718-7
只要 Observation 存在,就能判斷它是否符合這個集合。
所以不在集合內是 FAIL,不是尚未評估。
Day 10 新增或複用以下 fixture:
| fixture | 用途 | LAB-CODE-001 預期 |
|---|---|---|
valid-loinc-code.json |
Observation.code 包含 http://loinc.org 與允許 code 2345-7 |
PASS |
loinc-code-not-allowed.json |
Observation.code 是 LOINC,但 code 9999-9 不在允許集合 |
FAIL |
observation-code-without-coding.json |
Observation 有 code text,但沒有 coding | FAIL |
observation-coding-without-code.json |
Observation coding 有 system,但缺少 code | FAIL |
unsupported-resource-in-bundle.json |
Bundle 沒有 Observation | NOT_APPLICABLE |
這些 fixture 的目的不是追求案例數量。
它們的目的,是先把 LAB-CODE-001 的最小狀態分類固定下來:
PASS
FAIL
NOT_APPLICABLE
後續加入契約 YAML/JSON 載入時,才有穩定的規則行為可以比對。
Day 10 新增:
src/main/java/com/twlab/qualitygate/validation
└─ LabCode001ObservationLoincRule.java
規則仍然實作同一個介面:
public class LabCode001ObservationLoincRule implements ContractRule {
public static final String RULE_CODE = "LAB-CODE-001";
@Override
public String ruleCode() {
return RULE_CODE;
}
@Override
public List<RuleResult> validate(Bundle bundle) {
// ...
}
}
這代表 Day 7 建立的 ContractRule 介面仍然可以繼續使用。
第四條規則也不需要新的資料模型。
核心邏輯分成三步:
找出 Bundle 內所有 Observation
↓
逐一讀取 Observation.code.coding
↓
確認是否存在允許的 http://loinc.org coding
允許集合目前先寫在 class 裡:
private static final String LOINC_SYSTEM = "http://loinc.org";
private static final Set<String> ALLOWED_LOINC_CODES = Set.of("2345-7", "718-7");
判斷邏輯如下:
private boolean hasAllowedLoincCoding(Observation observation) {
if (!observation.hasCode() || !observation.getCode().hasCoding()) {
return false;
}
return observation.getCode().getCoding().stream()
.anyMatch(coding -> LOINC_SYSTEM.equals(coding.getSystem())
&& coding.hasCode()
&& ALLOWED_LOINC_CODES.contains(coding.getCode()));
}
這裡特別先檢查:
coding.hasCode()
原因是測試時發現,如果 coding 缺少 code,直接拿 null 去查 immutable Set 可能會造成不必要的例外。
規則應該回 FAIL,不應該讓單筆缺欄位資料把測試跑成 error。
Day 10 新增:
src/test/java/com/twlab/qualitygate/validation/LabCode001ObservationLoincRuleTests.java
測試一:Observation 有契約允許的 LOINC code
assertThat(results.get(0).ruleCode()).isEqualTo("LAB-CODE-001");
assertThat(results.get(0).outcome()).isEqualTo(RuleOutcome.PASS);
assertThat(results.get(0).path()).isEqualTo("Observation/obs-valid-loinc.code.coding");
assertThat(results.get(0).actual()).contains("http://loinc.org|2345-7");
這代表最基本的合作契約允許代碼可以通過。
測試二:Observation 有 LOINC,但不在契約允許集合
assertThat(results.get(0).outcome()).isEqualTo(RuleOutcome.FAIL);
assertThat(results.get(0).severity()).isEqualTo("error");
assertThat(results.get(0).actual()).contains("http://loinc.org|9999-9");
這代表問題不是 coding 格式不存在,而是 code 不符合本次交換契約。
測試三:Observation.code 沒有 coding
assertThat(results.get(0).outcome()).isEqualTo(RuleOutcome.FAIL);
assertThat(results.get(0).path()).isEqualTo("Observation/obs-no-coding.code.coding");
assertThat(results.get(0).actual()).isEqualTo("N/A");
這代表只有 code.text 不足以通過契約允許集合檢查。
測試四:Observation coding 缺少 code
assertThat(results.get(0).outcome()).isEqualTo(RuleOutcome.FAIL);
assertThat(results.get(0).actual()).contains("http://loinc.org|N/A");
這代表缺少 code 時會得到規則失敗,而不是程式例外。
測試五: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 10 測試的核心,不需要再另外截測試檔畫面。
指令:
./mvnw test
測試結果:
Tests run: 40, Failures: 0, Errors: 0, Skipped: 0
BUILD SUCCESS

Day 10 從 Day 9 的 35 個測試增加到 40 個測試。
新增的 5 個測試都集中在 LAB-CODE-001:
PASS
FAIL
FAIL
FAIL
NOT_APPLICABLE
LAB-CODE-001 不是完整 LOINC terminology validation。
它沒有查 terminology server,也沒有做 ValueSet expansion。
它只檢查:
Observation.code.coding 是否包含契約允許的 http://loinc.org code
所以文章裡應該稱為「契約允許集合檢查」,不要稱為完整術語驗證。
LOINC code 不能只看:
2345-7
還要確認 system 是:
http://loinc.org
否則不同 coding system 裡剛好相同的 code 字串可能被誤判。
NOT_EVALUATED
Day 10 已經有固定允許集合。
所以如果 code 不在:
2345-7
718-7
就應該回:
FAIL
Observation.code.text 可以提供人類可讀文字,但 Day 10 的契約檢查是 coding 集合。
所以只有:
"code": {
"text": "Glucose"
}
仍然回 FAIL。
缺少 coding.code 是資料問題,不應該造成規則引擎 error。
Day 10 測試中已固定這個行為:
http://loinc.org|N/A → FAIL
前三條規則回答的是:
資料鏈是否接得起來?
Day 10 的 LAB-CODE-001 回答的是:
接起來的 Observation,是否是合作方願意接收的檢驗項目?
也就是說,就算資料長這樣:
DiagnosticReport.result → Observation/obs-1
Observation.subject → Patient/patient-1
還是可能因為:
Observation.code.coding = http://loinc.org|9999-9
而被交換契約擋下。
這不是 JSON 格式錯誤。
也不一定是 TW Core Profile 錯誤。
它比較適合被歸類為:
Exchange Contract: FAILED
這也是資料品質閘門要補上的地方:
標準驗證告訴我們 Resource 結構是否合理。
交換契約規則告訴我們這份資料是否符合合作方接收條件。
BundleReferenceIndex。LAB-REF-001、LAB-REF-002、LAB-REF-003 使用共用 reference helper。LabCode001ObservationLoincRule。LabCode001ObservationLoincRuleTests。LAB-CODE-001 支援固定允許 LOINC:2345-7、718-7。LAB-CODE-001 對允許 LOINC 回 PASS。LAB-CODE-001 對不允許 LOINC 回 FAIL。LAB-CODE-001 對沒有 coding 或 coding 缺 code 回 FAIL。LAB-CODE-001 對沒有 Observation 的 Bundle 回 NOT_APPLICABLE。LAB-CODE-001 專用 fixture。./mvnw test 通過,測試數從 35 增加到 40。Day 10 尚未處理:
BundleParseService。RuleResult。LAB-UNIT-001 Quantity 可讀 unit。LAB-UNIT-002 UCUM system/code 契約允許集合。目前的規則進度:
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-001。
原因是目前已經能證明:
Reference 鏈正確
Observation.code 包含合作方允許 LOINC
但還不能判斷 Quantity 類型的檢驗數值是否有可讀單位。
完成 LAB-UNIT-001 後,再處理 LAB-UNIT-002,確認 UCUM system 與 code 是否符合合作契約。
Repository:twcore-data-quality-gate