摘要
Day 22 已經替 live FHIR/metadata入口補上 allowlist、redirect blocking、endpoint safety evidence,也讓 contract lifecycle 會影響 Quality Gate。Day 23 把 contract 裡已經宣告的 versionedValueSetcanonical URL 從「文件化 metadata」推進到 terminology server$expand/$validate-codeevidence。
假設某個檢驗 Bundle 裡的 Observation.code 是 2345-7。它存在於目前 contract 內建的 allowedLoincCodes snapshot,所以本地規則會通過。
但 partner contract 同時宣告了 versioned ValueSet canonical:
https://example.org/fhir/ValueSet/demo-lab-hospital-a-lab-codes|1.1
如果 terminology server 無法展開這份 ValueSet,或 $validate-code 回覆這個 code 不屬於該 ValueSet,那就不能只說「本地清單通過,所以可以交換」。
Day 22 先處理 live endpoint safety 和 contract lifecycle。Day 23 往交換語意再推一層:把 local snapshot rule 的結果,和 terminology server 對 $expand / $validate-code 的實際回覆分開呈現。
也就是說,Day 22 是 live endpoint safety gate;Day 23 是 terminology evidence gate。
目前專案的 contract 已經有:
"terminologyPolicy": {
"loincValueSetCanonical": "https://example.org/fhir/ValueSet/demo-lab-hospital-a-lab-codes|1.1",
"ucumValueSetCanonical": "https://example.org/fhir/ValueSet/demo-lab-hospital-a-lab-units|1.1",
"expansionTimestamp": "2026-08-21T00:00:00+08:00"
}
但 runtime 仍然主要依賴:
"allowedLoincCodes": ["2345-7", "718-7"],
"allowedUcumCodes": ["mg/dL", "mmol/L"]
Day 23 要處理的就是這個 gap:
ValueSet canonical 不應只停留在 report metadata。
它應該能產生 terminology server 查詢證據。
Day 23 的 request / report flow 改成這樣:
使用者上傳 Bundle
│
├─ 選擇 partner exchange contract
│ └─ 讀取 terminologyPolicy
│ ├─ loincValueSetCanonical
│ └─ ucumValueSetCanonical
│
├─ 執行原本的 local snapshot rules
│ ├─ LAB-CODE-001:allowedLoincCodes
│ └─ LAB-UNIT-002:allowedUcumCodes
│
└─ 如果有填 terminology server base URL
│
├─ endpoint guard
│ ├─ allowlist match
│ ├─ reject userinfo / query / fragment
│ ├─ reject localhost / private / link-local / metadata IP
│ └─ redirect 不 follow
│
├─ ValueSet/$expand
│ ├─ LOINC ValueSet canonical
│ └─ UCUM ValueSet canonical
│
└─ ValueSet/$validate-code
├─ Observation.code.coding
└─ Observation.valueQuantity.system/code
最後 Quality Test Report 會變成三層 evidence:
Quality Gate blocking result
├─ FHIR / TW Core / contract local rules
├─ Contract lifecycle evidence
└─ Terminology server evidence
├─ endpoint safety evidence
├─ ValueSet expansion evidence
├─ code validation evidence
└─ local snapshot fallback evidence
對使用者來說,Day 23 做的是五件事:
新增 optional terminology server base URL / allowlist config。
用 contract terminologyPolicy 呼叫 ValueSet/$expand。
用 Bundle 裡實際 LOINC / UCUM 呼叫 ValueSet/$validate-code。
保留 local allowed code snapshot fallback。
在 Quality Test Report 顯示 terminology server evidence。
實作上主要落在三個位置:configuration 負責 allowlist,validation service 負責 $expand / $validate-code evidence,首頁 report 負責把 server evidence 和 local snapshot fallback 分開呈現。
目前 LAB-CODE-001 和 LAB-UNIT-002 仍然是:
Observation.code 是否在 allowedLoincCodes 裡?
Observation.valueQuantity.code 是否在 allowedUcumCodes 裡?
這是好的 MVP fallback,但不是 terminology server validation。
Day 23 的正確定位是:
local snapshot = fallback / comparison evidence
terminology server = external terminology evidence
這比直接做 live Patient / Observation / DiagnosticReport lookup 更安全,因為 terminology check 不需要讀取 PHI resource endpoint,也不需要先設計完整 patient workflow。
Day 23 不把 Day 22 的 endpoint safety 重做一套低標準版本。
Terminology server base URL 也應該有類似設定:
qualitygate.terminology.server.allowed-base-urls=
qualitygate.terminology.server.allow-private-network=false
預設空 allowlist 表示:
terminology server evidence = NOT_EVALUATED
HTTP call = not called
local snapshot fallback = still available
Terminology endpoint guard 應拒絕:
http / https
169.254.169.254
這和 Day 22 的 metadata endpoint hardening 保持同一個安全模型。
$expand 的用途是確認 contract 宣告的 ValueSet 可以被 terminology server 展開。
Day 23 會針對 contract 裡的兩個 canonical URL 建立 expansion evidence:
| Contract field | Operation |
|---|---|
terminologyPolicy.loincValueSetCanonical |
GET {TERMINOLOGY_BASE}/ValueSet/$expand?url={canonical} |
terminologyPolicy.ucumValueSetCanonical |
GET {TERMINOLOGY_BASE}/ValueSet/$expand?url={canonical} |
成功時 report 應顯示:
$expand
如果 terminology server 回 OperationOutcome,report 不應只顯示 raw JSON。
應該萃取 severity / code / diagnostics,並把該 expansion 標成:
NOT_EVALUATED
原因是 terminology server 沒有成功提供可用 expansion。
$validate-code 的用途是確認 Bundle 裡實際出現的 code 是否屬於 contract 宣告的 ValueSet。
Day 23 先只針對現有 rule 已經處理的範圍:
| Bundle source | Code system | ValueSet |
|---|---|---|
Observation.code.coding |
http://loinc.org |
loincValueSetCanonical |
Observation.valueQuantity.system/code |
http://unitsofmeasure.org |
ucumValueSetCanonical |
每個實際 code 產生一筆 evidence:
$validate-code
PASS / FAIL / NOT_EVALUATED
例如:
Observation/obs-1.code.coding[0]
system = http://loinc.org
code = 2345-7
ValueSet = https://example.org/fhir/ValueSet/demo-lab-hospital-a-lab-codes|1.1
server result = PASS
local snapshot = PASS
UCUM 範例:
Observation/obs-1.valueQuantity
system = http://unitsofmeasure.org
code = mg/dL
ValueSet = https://example.org/fhir/ValueSet/demo-lab-hospital-a-lab-units|1.1
server result = PASS
local snapshot = PASS
Day 23 不應該在 terminology server timeout 時把原本可驗證的 Bundle 直接擋掉。
原因是目前 contract 的 allowedLoincCodes / allowedUcumCodes 仍然是本專案可重現的 local expansion snapshot。
所以 fallback policy 應該是:
| Terminology server state | Quality Gate behavior |
|---|---|
| 未設定 terminology server base URL | terminology layer NOT_EVALUATED,local snapshot rules 照跑 |
| base URL 不在 allowlist | terminology layer NOT_EVALUATED,local snapshot rules 照跑 |
$expand timeout / parse failed |
terminology layer NOT_EVALUATED,local snapshot rules 照跑 |
$validate-code timeout / parse failed |
該 code evidence NOT_EVALUATED,local snapshot rules 照跑 |
| server 明確回 code 不在 ValueSet | terminology evidence FAIL,但是否 blocking 仍由 contract SHALL/error policy 與 local rule 結果共同決定 |
Day 23 的保守做法:
terminology server evidence 本身先不直接取代 LAB-CODE-001 / LAB-UNIT-002。
它先成為獨立 report layer。
blocking 仍由現有 contract rules 決定。
這樣可以避免第一版 terminology integration 因外部 server 差異導致 gate 行為大幅波動。
Day 23 首頁新增:
Optional terminology server base URL
Terminology server evidence
ValueSet expansion evidence
Code validation evidence
Local snapshot fallback evidence
Quality Test Report 的 layer summary 新增一層:
| Layer | Blocking? | 說明 |
|---|---|---|
| Terminology server evidence | No for Day 23 | 呼叫 $expand / $validate-code,但第一版不取代 local snapshot blocking rules |
Terminology report 應該清楚區分:
server evidence = terminology server 實際回覆
local snapshot evidence = contract bundled allowed code arrays
這可以避免使用者誤解:
看到 allowedLoincCodes 通過,不等於 terminology server 已經通過。
本機 Maven 測試:
./mvnw test
結果:
Tests run: 92, Failures: 0, Errors: 0, Skipped: 0
BUILD SUCCESS
Day 23 新增測試覆蓋:
NOT_EVALUATED,且 HTTP client 不被呼叫。NOT_EVALUATED,且 HTTP client 不被呼叫。$expand 成功時產生 expansion evidence。$expand 成功時產生 expansion evidence。$expand 回 OperationOutcome 時轉成 clear diagnostics。$validate-code 成功時產生 PASS evidence。$validate-code 成功時產生 PASS evidence。$validate-code 明確回 false 時產生 FAIL evidence。NOT_EVALUATED evidence。測試 fixture 應該避免打真實 terminology server。
使用 fake TerminologyServerHttpClient 回傳固定 FHIR R4 JSON。
terminologyPolicy 不再只是 report metadata。$expand evidence。$expand evidence。$validate-code evidence。$validate-code evidence。./mvnw test 通過。Day 23 未處理:
Day 23 也不應把 terminology server 當成完整臨床判斷。
它只能回答:
這個 code 是否被 terminology server 判定屬於 contract 宣告的 ValueSet?
它不能回答:
這個檢驗值是否臨床合理?
這個單位和數值是否符合病人情境?
這個 code 是否應用在所有檢驗場景?
validation-flow
├─ JSON parse 完成
├─ FHIR R4 parse 完成
├─ FHIR R4 validation 完成
├─ TW Core validation / safe NOT_EVALUATED 完成
├─ Partner exchange contract loading
│ ├─ demo-lab-v1.0.json 完成
│ ├─ demo-lab-v1.1.json 完成
│ ├─ policyAssertions 完成
│ ├─ obligation / severity 完成
│ ├─ lifecycle metadata 完成
│ ├─ lifecycle runtime gate 完成
│ ├─ terminologyPolicy metadata 完成
│ ├─ allowedLoincCodes local snapshot 完成
│ ├─ allowedUcumCodes local snapshot 完成
│ ├─ optional uploaded contract 完成
│ └─ application-level schema validation 完成
├─ Exchange contract rules
│ ├─ LAB-REF-001 完成並由 contract 啟用
│ ├─ LAB-REF-002 完成並由 contract 啟用
│ ├─ LAB-REF-003 完成並由 contract 啟用
│ ├─ LAB-CODE-001 完成,允許值由 local snapshot 提供
│ ├─ LAB-UNIT-001 完成並由 contract 啟用
│ └─ LAB-UNIT-002 完成,允許值由 local snapshot 提供
├─ Quality Gate
│ ├─ SHALL error/fatal blocking 完成
│ ├─ SHOULD/MAY warning behavior 完成
│ └─ contract lifecycle blocking 完成
├─ Contract comparison
│ ├─ comparison service test 完成
│ ├─ homepage comparison display 完成
│ ├─ compare checkbox 完成
│ ├─ uploaded 2+ version comparison 完成
│ ├─ upgrade blocker evidence display 完成
│ ├─ compatibility classification 完成
│ └─ contract difference summary 完成
├─ Unit normalization evidence
│ ├─ glucose mg/dL -> mmol/L 完成
│ ├─ glucose mmol/L -> mg/dL 完成
│ └─ unsupported code/unit NOT_EVALUATED 完成
├─ Live FHIR API metadata
│ ├─ optional FHIR base URL 完成
│ ├─ endpoint allowlist 完成
│ ├─ endpoint safety evidence 完成
│ ├─ GET /metadata only 完成
│ ├─ CapabilityStatement parse 完成
│ ├─ fhirVersion alignment 完成
│ ├─ JSON format evidence 完成
│ └─ IG / profile declaration evidence 完成
├─ Terminology server evidence
│ ├─ optional terminology server base URL 完成
│ ├─ terminology endpoint allowlist 完成
│ ├─ ValueSet/$expand evidence 完成
│ ├─ ValueSet/$validate-code evidence 完成
│ └─ local snapshot fallback evidence 完成
├─ Scenario test pack
│ ├─ v1.0 / v1.1 representative cases 完成 4 例
│ └─ SHOULD warning behavior 完成 1 例
├─ Homepage Quality Test Report 完成
└─ Reproducible delivery
├─ Dockerfile 完成最小版
├─ Docker Compose 完成最小版並驗證啟動
└─ GitHub Actions CI 完成最小版
下一步預計處理:
live exchange sandbox auth and non-PHI resource interaction
Repository:twcore-data-quality-gate