# [18-2] 의약품 외형 비전 인식·DUR 처방 규칙 교차감사 소프트웨어(CDSS) 아키텍처 설계서

2026-11-14 · APOSTOLUS · 참조 구현 `dispense18/` · 실행 근거 `evidence/dispense18-run.json`, `evidence/dispense18-audit.json`

## 0. 범위와 결론

이 문서는 조제 현장에서 면허 약사의 최종 검수를 보조하는 **오프라인 조제 감사(audit) 소프트웨어**의 설계서다. 소프트웨어는 처방을 만들지도 바꾸지도 않고 투약을 지시하지 않는다. 하는 일은 하나다. 낱알 촬영 결과와 정형 처방 레코드를 대조해, 어긋나거나 판단할 수 없는 건을 **빠짐없이 약사의 강제 재검수 큐로 보낸다.**

- `AUTO_PASS`는 "강제 재검수 큐로 보내지 않는다"는 뜻이다. 약사의 통상 최종 검수는 그대로 남는다.
- 약품·성분·규칙은 모두 추상 코드(`PRD_0101`, `ING_A`, `DUR_COMBO_CI`)로 다룬다. 임계치는 외부 규칙표가 공급하며, 이 저장소의 값은 전부 가상 값이다.
- 영상 추론(분할·OCR·임베딩)은 이 저장소에서 실행하지 않았다. 해당 모듈은 인터페이스와 loss 수식으로 규정하고, 하네스는 그 **출력**을 합성 시뮬레이터로 만들어 매처·규칙 엔진·게이트·감사 로그를 실제로 실행했다.

**"미탐 0%"의 정의.** 확률 모델이 들어간 비전 경로의 미탐을 수학적으로 정확히 0이라고 증명하는 방법은 없다. 이 설계는 0%를 네 층으로 나누어 각각 다른 종류의 근거를 댄다.

| 층 | 주장 | 근거의 종류 | 합성 하네스 실측 |
| --- | --- | --- | --- |
| ① 결정론적 완전성 | 규칙표 범위 안의 금기·주의·판단불가 주문은 전부 재검수로 간다 | 추상 격자 전수 열거와 독립 오라클 차분 시험 | 22,410건 판정 전수 일치, 단조성 위반 0/27,000 |
| ② 관측 0건과 신뢰 상한 | 벤치마크 양성 N건 중 자동 통과 0건 | Clopper–Pearson 상한 $1-\delta^{1/N}$ | 양성 6개 분모 모두 0건 (§7) |
| ③ 비전 경로의 확률 상한 | 갤러리 안 오조제가 Gate B를 통과할 확률의 상한. 유사 외형 품목(품목 범주)은 품목 조건부로 $\le\alpha$, 그 밖(stratum 범주)은 보정 구성 가중 평균으로 $\le\alpha$이고 품목 하나로는 $\le\alpha/w_y$. 보정표에 대해 확률 $1-\delta$ 이상 (§4.2) | conformal 배제 검정, PAC 순서통계량 | $\alpha=0.01$, 범주별 $n\ge 554$ |
| ④ 무음 경로 부재 | 예외·시간 초과·결측·범위 밖 값·버전 불일치·기록 실패는 전부 재검수 | 상태기계 고장 조합 전수 열거(시간 초과는 단계가 보고한 tick 기준, §4.5) | 50,400조합 중 자동 통과 1개(무고장) |

보고는 항상 **(미탐 상한, 재검수 부담)** 쌍으로 한다. 재검수를 100%로 만들면 미탐은 자명하게 0이므로 한쪽만 적은 수치는 의미가 없다.

## 1. 시스템 구성과 IEC 62304 분해

```
[촬영 스테이션] → (V1) 분할·품질 → (V2) 각인 OCR → (V3) 속성·임베딩 ─┐  불신 입력
[처방/조제 레코드] → (N) 정규화 ──────────────────────────────────────┤
                                                                      ▼
                         ┌──────────── 감사 커널 (Class C) ────────────┐
                         │ INTEGRITY → MONITOR → ORDER → RULES →       │
                         │ CAPTURE → IDENT → LOCKOUT → CONFORMAL       │
                         │            → LEDGER commit                  │
                         └──────┬───────────────────────┬──────────────┘
                          AUTO_PASS(통상 검수)     REVIEW(강제 재검수 큐, 사유 코드)
```

| 소프트웨어 항목 | 제안 안전 등급 | 근거 |
| --- | --- | --- |
| 감사 커널(게이트, 규칙 엔진, 매처, 감사 로그) | Class C | 오판이 무음으로 지나가면 오조제가 환자에게 도달할 수 있다. 결정론 코드이며 전 경로를 열거·시험한다. |
| 비전 추론(V1–V3) | 커널과 분리된 항목. 자체 등급은 B 이상으로 관리 | 커널은 이 출력을 **신뢰하지 않는 입력**으로만 받는다. 형식·범위·완전성을 커널이 다시 검사하며, 비전 항목의 어떤 고장도 커널 안에서 `REVIEW` 이외의 결과를 만들 수 없음을 §4.5에서 보인다. |
| 규칙표·갤러리·보정표 | 구성 항목(configuration item) | SHA-256으로 고정하고, 커널이 매 건마다 고정값과 대조한다. |
| 학습 프레임워크·백본 | SOUP | 추론 산출물의 형식만 계약에 포함한다. |

분리의 요점은 "모델이 틀릴 수 있다"는 사실을 안전 논증에서 가정으로 쓰는 것이다. 모델 정확도는 재검수 부담을 줄일 뿐 미탐 상한의 근거가 아니다.

## 2. 산출물 1 — 미세 외형 비전 대조 엔진

### 2.1 참조 스키마 (식약처 낱알식별 공공DB → 갤러리 레코드)

| 갤러리 필드 | 원천 항목 | 용도 |
| --- | --- | --- |
| `code` | 품목기준코드 | ArcFace 클래스, 처방 대조 키 |
| `form` · `color` · `shape` | 제형, 색상(앞/뒤), 의약품 모양 | stratum = `form/color/shape`, 보조 head 정답, 결정론 속성 대조 |
| `line` | 분할선(앞/뒤) | 속성 대조, 분할 주의 규칙 입력 |
| `front` · `back` | 표시(각인) 앞/뒤 | 가중 편집거리 매칭 |
| `name` | 품목명 | 유사 명칭(sound-alike) 그래프 |
| `ref_image` | 공식 참조 이미지 | 갤러리 prototype $\mu_y$ 산출(추론 단계에서만 사용) |

알약 사진과 MIMIC-IV 처방 사이에 국내 품목 매핑을 가정하지 않는다. 외형 식별과 주문 감사는 정답도 분모도 따로 둔다(출처 등록부의 18-2 항).

### 2.2 촬영에서 커널 입력까지

1. **Instance segmentation.** 낱알마다 가시 마스크 $M_i$와 amodal 마스크 $\hat A_i$(가려진 부분까지 추정한 윤곽)를 낸다. 가림 비율은 $o_i = 1 - |M_i|/|\hat A_i|$, 트레이 값은 $o=\max_i o_i$다. $o>\theta_{occ}$이면 그 건은 강제 재검수다.
2. **수량 이중 추정.** $N_A=\#\{M_i\}$ (인스턴스 수), $N_B=\mathrm{round}\big(\sum_i|M_i|/A_{ref}(E)\big)$ (면적 합을 기대 품목의 단일 낱알 기준 면적으로 나눈 값, 또는 로드셀 중량÷단위 중량). 자동 통과에는 $N_A=N_B=q$(처방 수량)가 필요하다. 한 추정기의 오류가 수량 오류를 가리려면 다른 추정기도 같은 방향으로 틀려야 한다.
3. **조명 정규화.** 색 보정 카드로 flat-field와 white balance를 맞춘 뒤 마스크 내부 평균을 CIELAB으로 옮긴다. 색상군은 $\arg\min_g \Delta E_{00}(\bar c, c_g)$로 정하고, 최소 $\Delta E$가 한계를 넘으면 신뢰도를 낮춘다. 반사율은 $g = |\{p\in M: V_p>v_{hi}, S_p<s_{lo}\}|/|M|$로 재고 $g>\theta_{glare}$이면 강제 재검수다. 반사 화소는 OCR과 임베딩 입력에서 가린다.
4. **각인 OCR.** 마스크 주축으로 회전을 정규화하고 0°·180° 두 방향을 모두 읽어 신뢰도가 높은 쪽을 고른다. 출력은 면별 문자열과 최소 문자 신뢰도다. 어느 면이 앞인지는 믿지 않는다(§2.3의 면 순서 불변 거리).
5. **Multi-view 임베딩.** 앞·뒤 crop을 한 인코더에 넣어 $z\in\mathbb{S}^{D-1}$을 얻고, 갤러리 prototype과의 코사인 $s_y(x)=z^\top\mu_y$를 **사이트 갤러리 전 품목**에 대해 커널에 넘긴다.

커널이 받는 것은 `scores[|𝒢|]`, `ocr{front, back, conf}`, `attrs{form, color, shape, line, conf}`, `countA`, `countB`, `occlusion`, `glare`뿐이다. 길이가 다르거나 유한한 수가 아니면 커널은 예외로 처리하고 재검수로 보낸다.

### 2.3 각인 매칭 — 혼동 문자 가중 편집거리

혼동군 $\mathcal{K}$ = {O,0,Q,D}, {I,1,L}, {S,5}, {B,8}, {Z,2}, {G,6}, {U,V} (표는 외부 구성). 치환 비용은

$$c(a,b)=\begin{cases}0 & a=b\\ w_c & a\ne b,\ a\sim_{\mathcal K} b\\ w_h & \text{그 외}\end{cases},\qquad c_{ins}=c_{del}=w_h$$

이고 $d_w$는 이 비용의 Wagner–Fischer 거리다. 면 순서를 모르므로 두 면 거리는

$$D(o,r)=\min\{d_w(o_f,r_f)+d_w(o_b,r_b),\ d_w(o_f,r_b)+d_w(o_b,r_f)\}.$$

각인 통과 조건은 **가까움과 유일함** 두 가지다.

$$D(o,E)\le\tau_{imp}\quad\wedge\quad \min_{y\in\mathcal G\setminus\{E\}}D(o,y)-D(o,E)\ \ge\ \gamma_{imp}$$

비용은 정수(1/4 단위)로 두어 임계 비교에 부동소수 오차가 끼지 않게 한다. 하네스 값은 $w_c=1,\ w_h=4,\ \tau_{imp}=2,\ \gamma_{imp}=3$이다. 혼동 문자 두 개까지는 같은 각인으로 읽고, 혼동군 밖 치환·삽입·삭제 하나는 불일치다. 혼동 문자만 다른 두 품목(`S5`/`55`, `MD0`/`MDO`)은 서로 간 거리가 $\gamma_{imp}$보다 작아 어느 쪽도 유일성 조건을 통과하지 못한다. 이 쌍은 §4.3의 lockout으로 정적으로도 막는다.

### 2.4 학습 목적함수

$$\mathcal L=\mathcal L_{arc}+\lambda_{con}\mathcal L_{sup}+\lambda_{attr}\sum_{k\in\{form,color,shape,line\}}\mathrm{CE}_k+\lambda_{hn}\mathcal L_{hn}$$

- **ArcFace** (클래스 = 품목기준코드, 클래스별 margin $m_y$):
  $$\mathcal L_{arc}=-\frac1B\sum_i\log\frac{e^{s\,\phi(\theta_{i,y_i})}}{e^{s\,\phi(\theta_{i,y_i})}+\sum_{j\ne y_i}e^{s\cos\theta_{i,j}}},\quad \phi(\theta)=\begin{cases}\cos(\theta+m_{y})&\theta+m_y\le\pi\\ \cos\theta-m_y\sin m_y&\text{그 외}\end{cases}$$
  $m_y=m_0+m_\Delta\cdot\mathbb 1[y\in\text{LASA 군집}]$. 유사 외형 품목일수록 각도 여유를 더 요구한다. $\theta+m>\pi$ 구간의 보정식은 경계에서 값이 한 단계 내려가며(연속은 아니다) 단조 감소를 유지한다. 스칼라 기준 구현과 시험: `matcher.arcface_logit`.
- **Supervised contrastive** ($P(i)$ = 같은 품목의 다른 촬영):
  $$\mathcal L_{sup}=\sum_i\frac{-1}{|P(i)|}\sum_{p\in P(i)}\log\frac{\exp(z_i^\top z_p/\tau)}{\sum_{a\ne i}\exp(z_i^\top z_a/\tau)}$$
- **LASA hinge**: $\mathcal L_{hn}=\sum_{(a,p,n):\,n\in\mathcal N(a)}\max\big(0,\ z_a^\top z_n-z_a^\top z_p+m_{LASA}\big)$.
- 보조 head는 식약처 필드를 정답으로 쓴다. 추론 때 이 head의 출력은 **결정론 대조의 입력**이 된다(임베딩과 독립된 두 번째 장벽).

### 2.5 LASA hard negative mining

**혼동 그래프** $G=(\mathcal G,\mathcal E)$. 간선은 네 채널 중 하나라도 걸리면 생긴다.

| 채널 | 조건 | 하네스 값 |
| --- | --- | --- |
| 임베딩 근접(LOOK) | $\cos(\mu_a,\mu_b)\ge c_{look}$ | 0.85 |
| 각인 동치(IMPEQ) | 같은 stratum·같은 분할선이고 $D(a,b)<\gamma_{imp}$ | 3 (1/4 단위) |
| 명칭 유사(SOUND) | $\max\{1-\mathrm{Lev}(\pi(a),\pi(b))/\max|\cdot|,\ \mathrm{Dice}_2(\pi(a),\pi(b))\}\ge\theta_{name}$ | 0.75 |
| 검증 혼동 | 검증셋 혼동행렬에서 $\hat P(\hat y=b\mid y=a)$가 한계 이상 | 학습된 모델이 있을 때 추가 |

$\pi$는 한글 음절을 자모로 분해(`matcher.jamo`)한 뒤 발음이 가까운 자모·문자군을 대표 문자로 접는 사상이다.

**P×K 배치.** 품목 $P$개 × 품목당 $K$장. anchor를 뽑을 때마다 그래프에서 가장 가까운 이웃을 같은 배치에 넣고, 남은 자리가 하나뿐이면 이웃이 필요한 anchor는 뽑지 않는다. 그래서 *이웃이 있는 품목은 반드시 이웃과 함께 배치에 든다.* 하네스 실측(200배치, $P=8$): 이웃 동반 640/640, 배치 내 LASA 쌍 329개. 균등 추출은 동반 126/485, LASA 쌍 66개였다.

**커리큘럼.** 초반은 semi-hard($d(a,p)<d(a,n)<d(a,p)+m$)만, $E_1$ epoch 이후는 LASA 이웃 중 hardest($\arg\min_n d(a,n)$). 매 epoch 끝에 검증 혼동행렬로 그래프를 갱신한다.

### 2.6 모듈 인터페이스 (PyTorch, 규격)

아래는 인터페이스 규격이며 이 저장소에서 실행하지 않았다. 커널과의 계약은 반환 텐서의 shape과 의미다.

```python
class PillSegmenter(nn.Module):
    def forward(self, tray: Tensor) -> dict[str, Tensor]:
        """tray: (B, 3, H, W) 색 보정 후 영상.
        returns  masks: (N, H, W) bool 가시 마스크,  amodal: (N, H, W) bool,
                 batch_index: (N,) long,  score: (N,) float."""

class ImprintReader(nn.Module):
    def forward(self, crops: Tensor) -> tuple[list[str], Tensor]:
        """crops: (N, V=2, 3, h, w) 회전 정규화된 앞·뒤 crop.
        returns  면별 문자열 N×2,  conf: (N, 2) 문자 신뢰도의 최솟값."""

class PillEncoder(nn.Module):
    def __init__(self, backbone: nn.Module, feat_dim: int, embed_dim: int, n_form: int, n_color: int,
                 n_shape: int, n_line: int): ...
    def forward(self, views: Tensor, view_mask: Tensor) -> dict[str, Tensor]:
        """views: (B, V=2, 3, h, w),  view_mask: (B, V) bool (촬영되지 않은 면 False).
        f = backbone(views.flatten(0, 1)).view(B, V, feat_dim)
        w = attn(f).masked_fill(~view_mask[..., None], -inf).softmax(dim=1)     # 면 순서에 불변
        g = (w * f).sum(dim=1)
        returns  z: (B, embed_dim) L2 정규화,  form/color/shape/line: (B, n_*) logits."""

class ArcMarginHead(nn.Module):
    def __init__(self, embed_dim: int, n_classes: int, scale: float, class_margin: Tensor): ...
    def forward(self, z: Tensor, y: Tensor) -> Tensor:
        """z: (B, D), y: (B,) long → (B, C) logits. target 열에만 phi(theta) 적용, class_margin[y] 사용."""

class LasaBatchSampler(Sampler[list[int]]):
    def __init__(self, labels: Sequence[int], neighbours: dict[int, list[int]], P: int, K: int,
                 stage: Literal["semi_hard", "hardest"]): ...

def gallery_scores(z: Tensor, prototypes: Tensor) -> Tensor:
    """z: (B, D), prototypes: (|G|, D) 정규화 → (B, |G|) 코사인. 커널에는 한 건씩 list[float]로 전달."""
```

### 2.7 갤러리 밖 품목(open-set)

닫힌 갤러리 가정(A3) 아래에서만 §4.2의 상한이 성립한다. 갤러리에 없는 품목이 기대 품목과 임베딩상 가까우면 Gate B는 통과할 수 있다. 하네스에서 갤러리 밖 600건 중 220건이 Gate B만으로는 통과했고, 이들을 막은 것은 전부 Gate A(각인·속성)였다. 그래서 (i) 갤러리는 사이트 재고 목록 전체와 같아야 하고, (ii) 재고에 없는 품목의 유입은 입고 바코드·재고 대사 같은 비전 밖 통제로 막으며, (iii) 각인까지 같은 갤러리 밖 복제품은 이 소프트웨어가 탐지한다고 주장하지 않는다.

## 3. 산출물 2 — 처방·조제·환자 상태 교차감사 규칙·지식그래프 엔진

### 3.1 MIMIC-IV 컬럼 → 정규화 레코드

| 정규화 필드 | MIMIC-IV 원천 | 변환과 판단불가 조건 |
| --- | --- | --- |
| `line.product` | `hosp.prescriptions.gsn`(한 칸에 공백으로 나뉜 여러 코드 포함) / `ndc`(`0`은 공란) / `formulary_drug_cd` → 사이트 crosswalk | 어느 식별자도 crosswalk에 없거나 식별자들이 서로 다른 품목을 가리키면 `None`(미매핑). 성분·효능군 수준의 추상 crosswalk만 쓰고 국내 품목으로의 사상은 만들지 않는다. |
| `line.dose` · `line.unit` | `prescriptions.dose_val_rx`, `dose_unit_rx` | 단일 유한 수가 아니면(범위 표기, 자유 문자열, 공란) `None`. float로 옮겼다가 되돌렸을 때 원래 십진수와 달라지는 표기(자릿수 초과, 표현 범위 밖)도 `None`이다. 단위는 허용 목록(`TAB`,`CAP`,`MG`,`MCG`,`G`)만 환산한다. |
| `line.freq` | `prescriptions.doses_per_24_hrs` | 공란이면 `None`. `pharmacy.frequency`·`pharmacy.doses_per_24_hrs`로 보완하는 것은 사이트 구성 항목이며 참조 구현에는 없다(없으면 판단불가로 남는다). |
| `line.start` · `line.stop` | `prescriptions.starttime`, `stoptime` | 일 단위 정수. 허용 형식은 `YYYY-MM-DD`, 또는 날짜 뒤에 공백이나 `T`를 두고 `HH:MM[:SS[.ffffff]]`를 붙인 것뿐이다(정규식 `\d{4}-\d{2}-\d{2}(?:[ T]\d{2}:\d{2}(?::\d{2}(?:\.\d{1,6})?)?)?`에, 앞뒤 공백을 떼어 낸 문자열이 전체 일치하고 달력상 유효한 값). 그 밖의 문자열(뒤에 붙은 문자, 구분자 없는 표기, 시간대 표기)은 앞 열 글자로 읽지 않고 형식 오류로 거부한다. `stoptime` 공란이면 `None`. `pharmacy`의 기간 열로 보완하는 것은 사이트 구성 항목이며 참조 구현에는 없다. 종료가 시작보다 앞서는 줄은 구조 오류로 보고 규칙 단계를 예외(`RULES:EXCEPTION`)로 끝낸다. |
| 조제 실적 | `hosp.pharmacy.pharmacy_id`, `status`, `verifiedtime`, `fill_quantity`, `dispensation` | 주문 감사의 분모 구성에 사용 |
| 투약 실적 | `hosp.emar.emar_id`, `pharmacy_id`, `charttime`, `event_txt` | 중복·기간 계산의 실제 구간 보정(선택) |
| `patient.age` | `hosp.patients.anchor_age` + (기준 연도 − `anchor_year`) | 둘 중 하나라도 없거나 정수값이 아니면 `None`. 소수 연령은 버림하지 않는다(18.5세를 18세로 읽으면 연령 규칙이 CLEAR로 샌다). |
| `patient.sex` | `patients.gender` | `F`/`M` 외에는 `None` |
| `patient.scr` · `scrAgeDays` | `hosp.labevents`에서 `itemid = 50912`(혈청 creatinine), `valuenum`, `valueuom = mg/dL`, `charttime` | 기준 시각 이하(전체 타임스탬프 비교)에서 가장 최근에 기록된 creatinine 행 하나만 본다. 그 행을 쓸 수 없으면(단위가 다름, 수치 없음, 같은 시각에 상충하는 결과) 더 오래된 값으로 물러서지 않고 `None`. 시각 없이 날짜만 있는 행은 채택하지 않고, 그 날짜가 채택 후보의 날짜 이후(기준일 이하)이면 최신값을 확정할 수 없으므로 `None`. 시각을 해석할 수 없는 행이 있어도 `None`. 경과 일수는 타임스탬프 차이를 일 단위로 올림한다. 기준 시각은 전체 타임스탬프여야 한다. |
| 신장·체중·eGFR 보조 | `hosp.omr.result_name`, `result_value`, `chartdate` | 설계상 원천이다. 사이트가 직접 측정한 eGFR을 같은 신선도 규칙으로 쓰는 경로는 참조 구현에 없다. |
| `patient.pregnancy` | 정형 원천 없음 | 항상 `None`. 사이트가 공급하지 않는 한 임부 규칙은 판단불가다. |

참조 구현: `rules.mimic_line`(`prescriptions` 한 행), `rules.mimic_patient`(`patients` 한 행과 `labevents`의 creatinine 행). 위 표에서 참조 구현에 없다고 적은 원천은 읽지 않는다.

### 3.2 지식그래프

```
Product ──CONTAINS{strength_mg}──▶ Ingredient ──MEMBER_OF──▶ Class
Ingredient ──CONTRAINDICATED_WITH{rule_id}── Ingredient
Product ──LOOKALIKE{cos, imprint_gap}── Product          Product ──SOUNDALIKE{sim}── Product
Ingredient ──HAS_RULE──▶ Rule{id, type, params, source, version}
```

규칙표(`ruleset.json`)는 `version`과 파일 SHA-256으로 고정한다. 커널은 매 건마다 관측 SHA와 고정 SHA를 대조하고 다르면 `INTEGRITY:SHA_MISMATCH`로 재검수한다. 실제 운용 규칙표는 식약처·심평원 DUR 공개 규칙 코드에서 생성하며, 각 `Rule`에 원천 코드와 고시 버전을 넣는다.

### 3.3 규칙 유형과 3값 의미론

판정은 $\{\texttt{FLAG},\texttt{CLEAR},\texttt{UNKNOWN}\}$이고 결합은 strong Kleene OR다: 하나라도 FLAG면 FLAG, 아니면 하나라도 UNKNOWN이면 UNKNOWN, 아니면 CLEAR.

| 유형 | FLAG | UNKNOWN |
| --- | --- | --- |
| `DUR_COMBO_CI` 병용금기 | 금기 성분쌍의 투여 구간이 겹침 | 겹침 여부가 종료일 결측에 달림 |
| `DUR_AGE_CI` 연령금기 | `age < minAge` | 연령 결측 |
| `DUR_PREG_CI` 임부금기 | `pregnancy = YES` | 남성이 아니고 임신 여부 결측 |
| `DUR_DUP_ING` 동일성분 중복 | 같은 성분 두 줄의 구간이 겹침 | 겹침 불명 |
| `DUR_DUP_CLASS` 효능군 중복 | 지정 효능군의 서로 다른 성분이 겹침 | 겹침 불명 |
| `DUR_DOSE_MAX` 용량 상한 | 일일 환산량 > 상한 | 용량·횟수 결측, 미지원 단위 |
| `DUR_DURATION_MAX` 기간 초과 | `stop − start + 1 > maxDays` | 종료일 결측 |
| `DUR_ELDERLY_CAUTION` 노인주의 | `age ≥ minAge` | 연령 결측 |
| `DUR_SPLIT_CAUTION` 분할주의 | 분할 불가 품목의 1회 수량이 정수가 아님 | 수량 환산 불가 |
| `DUR_RENAL_ADJ` 신기능 보정 필요 | `eGFR < egfrBelow` | creatinine 결측·신선도 창 초과, 연령·성별 결측, 성인 미만 |

미매핑 품목이 한 줄이라도 있으면 FLAG가 아닌 모든 규칙은 UNKNOWN이다. 무엇이 들었는지 모르는 품목 앞에서 CLEAR라고 말할 근거가 없기 때문이다.

주문 줄의 구간은 `start ≤ stop`인 정수 일자여야 한다. 역전된 구간은 판정하지 않고 예외로 끝낸다. 초안은 이를 거르지 않아 기간 규칙이 음수 일수를 "한도 이내"로 읽고 CLEAR를 냈다(BC65, `test_reversed_interval_is_rejected_not_cleared`).

용량 비교는 정확 십진 산술(`Decimal`)로 한다. 단위 환산(`MCG = 0.001 MG`, `G = 1000 MG`)이 임계값과 정확히 같을 때 부동소수 오차로 판정이 뒤집히지 않게 하기 위함이다. 곱셈은 유효숫자 120자리 문맥에서 하고 반올림이 생기면(`Inexact`) 그 값으로 비교하지 않고 UNKNOWN을 낸다. 기본 28자리 문맥은 17자리 수 두 개의 곱부터 반올림하므로 쓰지 않는다.

**eGFR.** CKD-EPI 2021(인종 계수 없음), 성인에만 적용:

$$\mathrm{eGFR}=142\cdot\min(S_{cr}/\kappa,1)^{a}\cdot\max(S_{cr}/\kappa,1)^{-1.200}\cdot0.9938^{\text{age}}\cdot(1.012\ \text{if F}),\quad(\kappa,a)=\begin{cases}(0.7,-0.241)&\text{F}\\(0.9,-0.302)&\text{M}\end{cases}$$

**정리 R1 (정보 단조성).** 환자 사실 하나를 결측으로 바꾸면 각 규칙의 판정은 그대로이거나 UNKNOWN이 된다. 따라서 입력이 줄어들어 재검수 건이 통과 건으로 바뀌는 일은 없다.

*근거.* 각 규칙은 결측을 기본값으로 메우지 않고 UNKNOWN으로만 답한다. 하네스는 격자 P의 모든 사례에 대해 한 필드를 지운 사례와 규칙별로 대조했다(27,000쌍, 위반 0). 설계 중 실제로 걸러낸 반례가 하나 있다. "임신 여부가 결측이어도 가임 연령대 밖이면 CLEAR"라는 편의 규칙은, 기록상 `YES`인 사례에서 임신 필드만 지우면 FLAG가 CLEAR로 바뀐다. 이런 **가정 기반 기본값은 커널에 두지 않는다**(시험: `test_no_default_assumption_can_clear_a_recorded_flag`).

**정리 R2 (범위 안 완전성).** 솔버(Kleene 결합기, 십진 산술)와 감사기의 오라클(가능세계 의미론, 유리수 산술)은 추상 격자 22,410건 × 규칙 11개에서 판정이 전부 같다.

오라클은 결측 사실마다 가능한 값을 모두 대입해, 모든 세계에서 규칙이 걸리면 FLAG, 어느 세계에서도 걸리지 않으면 CLEAR, 그 밖은 UNKNOWN으로 답한다. 격자는 환자 격자 P(연령 7 × 성별·임신 5 × 신기능 5 × 주문 조합 54 = 9,450)와 용량·기간 격자 D(기간 4 × 용량·단위 6 × 구간 관계 3 × 순서 있는 주문 조합 90 × 환자 2 = 12,960)다. 두 격자의 직적이 아니라 **규칙의 의존 집합별 전수**다. 규칙표에 없는 임상 지식의 누락은 이 정리가 다루지 않는다. 그것은 규칙표 원천과 버전 관리의 문제다.

## 4. 산출물 3 — 2단 Fail-Safe Gate

### 4.1 Gate A (결정론)

$$A(x,\text{order})=\underbrace{\textstyle\bigwedge_r v_r=\texttt{CLEAR}}_{\text{RULES}}\wedge\underbrace{N_A=N_B=q\ \wedge\ o\le\theta_{occ}\ \wedge\ g\le\theta_{glare}}_{\text{CAPTURE}}\wedge\underbrace{D(o,E)\le\tau_{imp}\wedge\mathrm{gap}\ge\gamma_{imp}\wedge\text{attrs}=\text{attrs}(E)\wedge\mathrm{conf}\ge c_{min}}_{\text{IDENT}}\wedge\underbrace{E\notin\mathcal L}_{\text{LOCKOUT}}$$

여기에 `INTEGRITY`(규칙표·갤러리·게이트 구성 SHA 일치, 보정표 유효기간), `MONITOR`(drift latch 꺼짐), `ORDER`(수량이 양의 정수, 첫 주문 줄이 기대 품목, 기대 품목이 갤러리에 있음)가 앞선다.

**비교에 앞선 유효 범위 검사.** 비전 출력은 임계값과 비교하기 전에 값의 범위부터 본다. 가림·반사 비율과 OCR·속성 신뢰도는 $[0,1]$의 유한 실수(불리언 불가), 수량 추정값은 음이 아닌 정수, 각인 판독은 문자열, 코사인 점수는 $[-1,1]$(반올림 여유 $10^{-9}$)의 유한 실수여야 한다. 하나라도 벗어나면 그 단계는 예외로 끝나고 재검수다. 초안은 이 검사가 없어 신뢰도 `True`나 $+\infty$가 "하한 이상"으로, 음수 가림 비율이 "임계 이하"로 통과했다(BC58–BC64, BC66, `test_out_of_range_vision_values_are_rejected_before_any_comparison`).

### 4.2 Gate B (conformal 배제 검정)

**귀무가설을 오조제 쪽에 둔다.** 갤러리 품목 $y$마다 "$H_y$: 트레이의 낱알은 $y$다"를 검정한다. 통계량은 부호 있는 margin

$$m_y(x)=s_y(x)-\max_{y'\ne y}s_{y'}(x).$$

범주 사상 $\kappa:\mathcal G\to\mathcal C$ (Mondrian taxonomy)를 두고, 범주 $c$의 보정 집합은 **참 쌍**의 margin $\{m_{y_i}(x_i):\kappa(y_i)=c\}$, 크기 $n_c$다.

$$k_c=\max\{k:\ \Pr[\mathrm{Bin}(n_c,\alpha)\le k-1]\le\delta_c\},\qquad\theta_c=m_{(k_c)}\ (\text{k번째로 작은 값}),\qquad C(x)=\{y:\ m_y(x)\ge\theta_{\kappa(y)}\}$$

$$B(x,E)=\big[C(x)=\{E\}\big]\ \wedge\ \big[\arg\max_y s_y(x)=E\big]\ \wedge\ \big[m_E(x)\ge\tau_m\big],\qquad \texttt{AUTO\_PASS}\iff A\wedge B$$

**정리 B1 (주변 타당성).** 범주 $c$의 시험 쌍 $(X,Y)$가 보정 쌍과 교환가능하면 $p_Y(X)=\frac{1+\#\{i:m_i\le m_Y(X)\}}{n_c+1}$에 대해 $\Pr[p_Y(X)\le\alpha\mid\kappa(Y)=c]\le\alpha$.

**정리 B2 (PAC, 보정 조건부).** 가정: 범주 $c$의 보정 margin $M_1,\dots,M_{n_c}$와 시험 margin $M$이 분포 $F_c$에서 **독립·동일분포**로 나온다. 교환가능성만으로는 B1까지만 성립한다. $F_c^{-1}(u)=\inf\{m:F_c(m)\ge u\}$로 두면 독립 균등 $U_i$로 $M_i=F_c^{-1}(U_i)$라 쓸 수 있고, $F_c^{-1}$이 단조이므로 $M_{(k)}=F_c^{-1}(U_{(k)})$다. $m<F_c^{-1}(u)$이면 $F_c(m)<u$이므로 모든 $u$에서 $F_c\big(F_c^{-1}(u)^-\big)\le u$다. 그러므로 원자가 있는 분포에서도

$$\Pr[M<\theta_c\mid\text{cal}]=F_c\big(M_{(k_c)}^-\big)\le U_{(k_c)},\qquad \Pr[U_{(k)}>\alpha]=\Pr\big[\#\{i:U_i\le\alpha\}\le k-1\big]=\Pr[\mathrm{Bin}(n_c,\alpha)\le k-1]\le\delta_c$$

이고, $\{Y\notin C(X)\}=\{m_Y(X)<\theta_{\kappa(Y)}\}$이므로

$$\Pr_{\text{cal}}\Big[\Pr\big[Y\notin C(X)\,\big|\,\kappa(Y)=c,\ \text{cal}\big]\le\alpha\Big]\ \ge\ 1-\delta_c.$$

*층화 보정에 대한 보충.* 하네스의 stratum 범주는 품목마다 같은 장수를 뽑는다. 보정 margin은 서로 독립이지만 품목별 분포 $F_y$가 달라, 혼합 분포 $F_c=\sum_yw_yF_y$ ($w_y=n_y/n_c$)에서의 동일분포 표본은 아니다. 이때는 $t_0=\sup\{t:F_c(t^-)\le\alpha\}$로 두면 $\{F_c(\theta_c^-)>\alpha\}=\{\theta_c>t_0\}=\{S\le k_c-1\}$이고, $S=\#\{i:M_i\le t_0\}$는 평균이 $n_cF_c(t_0)\ge n_c\alpha$인 Poisson-binomial이다. Poisson-binomial은 같은 평균의 이항분포보다 평균 둘레에 더 모이므로(Hoeffding 1956, *Ann. Math. Statist.* 27, 정리 4) $k_c\le n_c\alpha$이면 $\Pr[S\le k_c-1]\le\Pr[\mathrm{Bin}(n_c,\alpha)\le k_c-1]\le\delta_c$가 유지된다. 감사기는 모든 범주에서 $k_c\le n_c\alpha$를 확인한다(하네스: $k_c=1$, $n_c\alpha\approx6.4$).

$\delta_c=\delta/|\mathcal C|$로 두면 **모든 범주가 동시에** 성립할 확률이 $1-\delta$ 이상이다. $k=1$조차 조건을 못 채우는 크기, 곧 $n_c<n_{min}=\lceil\ln\delta_c/\ln(1-\alpha)\rceil$인 범주는 **미인증**이다. 미인증 범주는 $\theta_c$가 없어 그 품목들이 항상 $C(x)$에 남고, 커널은 미인증 범주가 하나라도 있으면 모든 건에 `CONFORMAL:UNCERTIFIED`를 낸다(자동 통과 전면 중단).

**따름정리 B3 (무음 실패 상한).** 실제 낱알이 $y\ne E$인데 `AUTO_PASS`가 났다면 $C(X)=\{E\}$이므로 $y\notin C(X)$, 곧 $m_y(X)<\theta_{\kappa(y)}$다. 이 사건에는 기대 품목 $E$가 들어가지 않는다. 그래서 어떤 품목이 어떤 품목으로 잘못 집히는지(오류 기전)를 몰라도 아래가 성립한다. A1과 B2의 가정 아래, 보정표에 대해 확률 $1-\delta$ 이상으로:

- **(a) 품목 범주** ($\kappa(y)=\{y\}$, 유사 외형 품목):
  $$\Pr[\texttt{AUTO\_PASS}\wedge E\ne y\mid Y=y,\ \text{cal}]\ \le\ \Pr[m_y(X)<\theta_y\mid Y=y,\ \text{cal}]\ \le\ \alpha.$$
  품목 조건부 상한이다.
- **(b) stratum 범주** $c$ (보정 구성 가중치 $w_y=n_y/n_c$):
  $$\sum_{y\in c}w_y\,\Pr[\texttt{AUTO\_PASS}\wedge E\ne y\mid Y=y,\ \text{cal}]\ \le\ \sum_{y\in c}w_y\,\Pr[m_y(X)<\theta_c\mid Y=y,\ \text{cal}]\ \le\ \alpha.$$
  상한은 **보정 구성으로 가중한 평균**에 대한 것이다. 품목 하나로 보면 $\Pr[\texttt{AUTO\_PASS}\wedge E\ne y\mid Y=y,\text{cal}]\le\alpha/w_y$(균등 구성이면 $|c|\,\alpha$)까지 열려 있다. 두 식의 좌변은 무음 실패 사건, 곧 실제 품목이 $y$인데 기대 품목은 $y$가 아니면서 자동 통과한 경우다. 기대 품목이 실제와 같은 정상 조제의 자동 통과($E=y$)는 이 사건에 들지 않으며 상한의 대상도 아니다. 운용에서 오조제가 보정 구성과 다른 비율로 특정 품목에 몰리면, 그 실제 가중 평균은 $\alpha$를 넘을 수 있다.

Gate A와의 독립은 가정하지 않는다. 인증하는 것은 위 두 상한이고, Gate A는 그 위에 얹는 별개의 장벽이다.

**Taxonomy는 혼합형이다.** 혼동 그래프에 LOOK 간선이 있는 품목은 자기 자신이 범주($\kappa(y)=\texttt{C:}y$), 나머지는 stratum이 범주($\kappa(y)=\texttt{S:}\,form/color/shape$)다.

- 품목 범주에는 B3 (a)가 붙는다. 오조제가 몰리는 유사 외형 품목은 절대 pooling하지 않는 이유다.
- stratum 범주에는 B3 (b)만 붙는다. 하네스의 stratum 범주는 품목 3–6개를 같은 장수로 묶었으므로 품목별 최악 상한은 $3\alpha$–$6\alpha$(3–6%)다. stratum 범주는 신규·저빈도 품목의 초기 보정 수단이고, 확인된 촬영이 $n_{min}$장 쌓인 품목은 품목 범주로 승격한다.
- 승격은 소급 보장이 아니다. 품목 조건부 상한은 승격 뒤 새 보정표로 내린 판정에만 붙고, 승격 전에 stratum 표로 내린 판정의 보장은 (b) 그대로다. 승격에 쓰는 촬영은 A1·B2의 가정(독립, 같은 촬영 조건)을 채워야 하며 재검수로 넘어간 건만 골라 쓰면 안 된다.
- 하네스의 ablation: 유사 외형 품목 참 쌍 1,604건의 비포함(오배제)이 혼합형 3건, 전부 stratum으로 묶으면 6건이었다. 둘 다 $\alpha$ 아래지만, 혼합형을 택한 이유는 이 수치가 아니라 위의 품목 조건부 논증이다.

품목 범주의 $\theta_c$는 음수가 될 수 있다(유사 외형 쌍은 참 쌍 margin 자체가 작다). 그러면 유사 품목은 1등이 아니어도 $C(x)$에 남아 `SET_MULTI`가 된다. 보수적인 쪽으로 작동하는 성질이며 경계 사례 BC57로 고정했다.

### 4.3 LASA lockout

$$\mathcal L=\Big\{E:\ \exists y\ne E,\ \big[\text{같은 stratum}\wedge\text{같은 분할선}\wedge D(E,y)<\gamma_{imp}\big]\ \vee\ \cos(\mu_E,\mu_y)\ge c_{lock}\Big\}$$

**이중 장벽 원칙.** 자동 통과는 모든 대안 품목 $y$에 대해 결정론 장벽(각인·속성)과 확률 장벽(임베딩) **둘 다**가 $E$와 $y$를 가를 수 있을 때만 허용한다. 어느 한쪽이 원리상 가를 수 없는 이웃이 있으면 그 품목은 lockout이다.

**정리 L1.** $E\in\mathcal L$인 주문에서 `AUTO_PASS` 확률은 정확히 0이다(`LOCKOUT:LOCKED`가 항상 붙는다). 이 경로의 무음 실패는 통계적 상한이 아니라 구조적으로 없다. 대가는 그 품목의 재검수율 100%다(하네스: 310/310건 재검수).

### 4.4 Drift 감시와 latch

확인된 촬영 $(x_t,y_t)$가 올 때마다 **온라인** conformal p-value를 만든다. 품목 $y$의 기준 다중집합 $R_y$는 그 품목의 보정 margin으로 시작한다. 새 촬영의 margin $m_t=m_{y_t}(x_t)$를 *그 시점의* $R_{y_t}$에 대한 평활 순위로 바꾸고, 그 **뒤에** $m_t$를 $R_{y_t}$에 넣는다.

$$p_t=\frac{\#\{r\in R_{y_t}:r<m_t\}+u_t\big(\#\{r\in R_{y_t}:r=m_t\}+1\big)}{|R_{y_t}|+1},\quad u_t\sim U(0,1);\qquad R_{y_t}\leftarrow R_{y_t}\uplus\{m_t\}$$

분모의 $|R_{y_t}|$는 그 시점의 크기, 곧 보정 장수에 그때까지 확인된 그 품목의 촬영 수를 더한 값이다. 검정 martingale은 power martingale의 혼합이다.

$$M_t=\frac1{|\mathcal E|}\sum_{\epsilon\in\mathcal E}\prod_{s\le t}\epsilon\,p_s^{\epsilon-1}$$

**정리 D1 (Ville).** 가정: (i) 품목마다 [보정 촬영, 이후 확인 촬영]의 margin 열이 교환가능하고 촬영들은 서로 독립이다(품목별 분포가 고정), (ii) $u_t$는 독립 균등이다, (iii) 감시에 넣는 촬영은 점수와 무관하게 정해진다(전수 또는 무작위 표본). 그러면 $p_1,p_2,\dots$는 도착 순서대로 서로 독립이고 $U(0,1)$을 따른다(온라인 평활 conformal p-value의 표준 결과, Vovk·Nouretdinov·Gammerman 2003 "Testing exchangeability on-line"; 여기서는 품목별로 나눈 Mondrian 형태). 따라서 $\mathbb E[\epsilon p_t^{\epsilon-1}\mid\mathcal F_{t-1}]=\int_0^1\epsilon p^{\epsilon-1}dp=1$이고, $M_t$는 $M_0=1$인 음이 아닌 martingale이며 $\Pr[\sup_tM_t\ge1/\eta]\le\eta$다. 구현은 $\log p$의 발산을 막으려고 $p$를 $10^{-12}$ 아래에서 자른다. $0<\epsilon\le1$에서는 이 절단이 내기 값을 줄일 뿐이어서 $\mathbb E[\epsilon\max(p,a)^{\epsilon-1}]=1-(1-\epsilon)a^{\epsilon}\le1$이고, 구현된 과정은 음이 아닌 supermartingale이다. Ville 부등식은 supermartingale에도 성립하므로 상한 $\eta$는 보수적으로 유지된다. $\epsilon>1$이면 절단 뒤 기대값이 1을 넘으므로 지수 격자가 $(0,1]$ 밖이면 감시 객체 생성을 거부한다. $M_t\ge1/\eta$이면 latch를 걸고 이후 모든 건을 `MONITOR:DRIFT_LATCH`로 재검수한다. latch는 스스로 풀리지 않는다. 재보정과 책임자 서명 기록 뒤에만 해제한다.

가정 (iii)은 운용 조건이다. 이 작업 흐름에서는 `AUTO_PASS` 건도 약사의 통상 최종 검수를 거치므로 모든 촬영이 확인 라벨을 얻는다. 강제 재검수로 넘어간 건만 감시에 넣으면 margin이 작은 쪽으로 치우친 표본이 되어 균등성이 깨진다.

**초안의 결함과 수정.** 초안은 고정된 보정표에 대해 매 촬영의 p-value를 계산했다. 그 구성에서는 연속한 p-value가 같은 보정표를 공유해 서로 의존하고, 보정표가 주어졌을 때 균등하지도 않다. 조건부 기대값이 1이라는 요건이 없으므로 Ville 부등식을 쓸 수 없다. 교차검증의 지적을 받아 위의 온라인 방식으로 바꿨다. 값 네 개의 24가지 순서 전부에서 온라인 순위의 결합 분포는 가능한 $2\times3\times4$가지 조합이 정확히 한 번씩 나오고(균등·독립), 표를 키우지 않는 방식의 비교는 함께 움직인다는 것(세 비교가 모두 한쪽인 순서가 3이 아니라 6)을 단위 시험으로 고정했다. 감시 객체는 자기 기준 상태를 소유한다(`conformal.DriftMonitor`). 커널 컨텍스트의 얕은 복사본들이 상태를 공유하지 않는다. 게이트의 보정표($\theta_c$)는 그대로 고정이며 B2의 보장과 별개다.

**기준은 품목별로 둔다.** 범주 단위로 묶은 기준은 "범주 안에서 교환가능"을 요구하는데, 그날의 처방 구성이 보정 구성과 다르면 이 가정이 깨진다. 본 벤치마크(drift 없음, LASA 주입으로 구성이 치우친 흐름)의 5,845회 갱신에서 품목별 기준과 범주별 기준 모두 $M_t$가 1을 넘지 않았다(최대 $\ln M_t=0$, 경보선 $\ln100=4.61$). 이 실행에서는 두 기준의 차이가 관측되지 않았다. 품목별 기준을 택한 근거는 수치가 아니라 가정의 구조다. 초안의 고정 pooled 표는 같은 흐름에서 경보($\ln M=4.99$)를 냈지만, 타당하지 않은 구성에서 나온 값이므로 근거로 쓰지 않는다.

**탐지 지연과 검정력.** 온라인 방식은 latch 전까지 변화 이후의 margin도 기준에 받아들인다. 기준이 오염될수록 이후의 p-value는 균등 쪽으로 돌아가므로, 약하고 오래 가는 변화는 늦게 잡히거나 끝내 잡히지 않을 수 있다. 타당성의 대가다. 하네스 실측:

| 시나리오 | 변화 시점 | latch | 변화 후 latch 전 건수 | 그 사이 자동 통과 | 그중 오조제 자동 통과 |
| --- | ---: | ---: | ---: | ---: | ---: |
| 잡음 2배 | 300 | 342 | 43 | 22 | 0 / 6 |
| 잡음 1.25배 | 300 | 796 | 497 | 253 | 0 / 92 |

latch 전 구간에서는 B3의 상한이 보장되지 않는다. 약한 변화에서는 그 구간이 500건 가까이 이어졌다. 그래서 (a) 라벨이 필요 없는 선행 감시(top-1 margin 분포의 KS 검정, `SET_EMPTY`·`LOW_MARGIN` 비율), (b) 일일 색 보정 카드 점검, (c) 보정표 유효기간(`calibMaxAgeDays`)에 따른 주기적 재보정을 함께 둔다. (a)는 설계 요구사항이며 하네스에는 구현하지 않았다.

### 4.5 상태기계와 fail-closed

각 단계는 `("PASS", [], ticks)` 또는 `("FAIL", [사유…], ticks)`만 반환할 수 있다.

| 단계의 실제 거동 | 커널의 해석 |
| --- | --- |
| 형식에 맞는 PASS, 기한 내 | 통과 |
| 형식에 맞는 FAIL | 사유 수집 |
| 예외 발생 | `<단계>:EXCEPTION` |
| `ticks > deadline` (결과가 PASS여도) | `<단계>:TIMEOUT` |
| 그 밖의 모든 반환(None, 사유 달린 PASS, 사유 없는 FAIL, 형이 다른 값, 음수이거나 정수가 아닌 ticks) | `<단계>:MALFORMED` |
| 단계 함수 자체가 없음 | `<단계>:EXCEPTION` |

모든 단계를 끝까지 돌려 사유를 전부 모은 뒤(약사가 한 번에 본다) 결정한다. 그다음 감사 기록을 commit한다. commit이 `True`를 반환하지 않으면 결정은 `REVIEW`, 상태는 `SAFE_HALT`다. **원장이 연결되지 않은 커널의 commit은 실패다.** 원장 없이 구성된 커널은 정상 촬영에서도 `LEDGER:COMMIT_FAIL`로 끝난다(시험: `test_kernel_without_a_trail_cannot_release`). 초안 구현은 원장이 없으면 commit을 성공으로 돌려주었고, 본 벤치마크가 그 경로로 돌고 있었다. 검수에서 이 기본값을 fail-closed로 바꿨다.

```
              모든 단계 PASS ∧ commit 성공
  DECIDING ──────────────────────────────▶ RELEASED   (AUTO_PASS, 통상 검수로)
     │  사유 ≥ 1 ∧ commit 성공
     ├───────────────────────────────────▶ QUEUED     (REVIEW, 강제 재검수 큐)
     │  commit 실패(사유 유무 무관)
     └───────────────────────────────────▶ SAFE_HALT  (REVIEW, 스테이션 정지·수기 절차)
```

**시간 초과가 뜻하는 범위.** 하네스에서 시간은 단계가 스스로 보고하는 논리 tick이다. 커널은 실제 실행 시간을 재지 않고 멈춘 단계를 중단하지도 않는다. 값을 돌려주지 않는 단계가 있으면 이 구현은 그대로 기다린다. 아래 열거가 보이는 것은 "보고된 tick이 기한을 넘으면 그 단계의 결과를 버리고 재검수로 보낸다"는 제어 논리까지다. 실제 시간 제한은 운용 요구사항으로 둔다.

- **REQ-FS-02.** 단계마다 커널 밖의 watchdog(별도 프로세스 또는 하드웨어 타이머)이 기한을 잰다. 만료되면 단계를 중단하고 `<단계>:TIMEOUT` 사유를 넣는다.
- **REQ-FS-03.** 하류(반출 인터록)는 commit된 결정 기록이 `AUTO_PASS`인 건만 통상 검수로 넘긴다. 기한 안에 결정 기록이 없는 건은 `REVIEW`로 취급한다. 무응답은 통과가 아니다.

**정리 F1.** 종단 상태는 위 셋뿐이며 `RELEASED`에 이르는 경로는 "8개 단계가 모두 형식에 맞는 PASS를 (보고된 tick 기준으로) 기한 안에 반환하고 감사 기록이 commit된" 하나다.

*근거.* 9개 차원의 고장 코드 3·2·3·5·5·4·2·7·2 = **50,400조합**을 전부 실행했다. `RELEASED` 1(전 차원 `OK`), `QUEUED` 25,199, `SAFE_HALT` 25,200. 감사기는 파이프라인 코드를 쓰지 않고 고장표에서 기대 결정·사유·상태를 직접 만들어 전 조합의 digest가 같은지 본다. 이 대조가 개발 중 실제 불일치를 하나 잡았다. `None`을 반환한 단계가 `MALFORMED`가 아니라 `EXCEPTION`으로 기록되던 것이다. 결정은 두 경우 모두 `REVIEW`여서 안전에는 영향이 없었고 사유 코드만 달랐다. 고쳤다.

### 4.6 벤치마크에서 0%를 말하는 규약

- 분모는 종류별로 따로 둔다: 외형 식별 양성(LASA 교체, 임의 교체, 갤러리 밖), 수량 오류, 주문 감사 양성(규칙 FLAG, 판단불가).
- 양성 $N$건에서 미탐 0건이면 보고값은 "0/N, 단측 $1-\delta$ 상한 $1-\delta^{1/N}$"이다. $N=0$이면 비율을 계산하지 않고 `HOLD_EMPTY_DENOMINATOR`다(100%를 반환하지 않는다).
- 상한 $p_0$를 주장하려면 양성이 $N\ge\ln\delta/\ln(1-p_0)$건 필요하다. $\delta=0.05$에서 $p_0=10^{-2}$: 299건, $10^{-3}$: 2,995건, $10^{-4}$: 29,956건.
- 같은 표에 재검수율과 자동 통과율을 반드시 함께 적는다. 각 장벽 단독 통과 수(`passA`, `passB`)도 적어 어느 장벽이 실제로 막았는지 보인다.

### 4.7 가정과 위반 대응

| 가정 | 내용 | 위반 감지 | 대응 |
| --- | --- | --- | --- |
| A1 교환가능성과 독립성 | 보정 촬영과 운용 촬영의 $X\mid Y=y$가 같다(촬영 장치·조명 프로토콜 고정, 품질 단계 통과분끼리). B1과 D1에는 교환가능성으로 충분하다. B2·B3의 PAC 상한에는 보정 쌍이 **서로 독립**이고 범주 안에서 같은 분포(층화 설계면 품목별 고정 분포)라는 가정이 더 필요하다. 같은 낱알을 연속 촬영한 프레임처럼 서로 의존하는 자료는 이를 깨므로 보정 촬영은 낱알·제조번호·날짜를 바꿔 취득한다. | §4.4 martingale, 색 보정 카드 일일 점검, 구성 SHA, 보정 취득 기록(낱알·제조번호·일자) 검토 | latch → 전건 재검수 → 재보정 |
| A2 보정 라벨 정확성 | 보정 쌍의 품목 라벨이 맞다 | 보정 촬영은 바코드로 확인한 원병에서 취득, 이중 확인. 범주별 margin 하위 꼬리의 이상값 검토 | 오염이 의심되는 범주는 미인증으로 강등 |
| A3 갤러리 폐쇄성 | 트레이에 오를 수 있는 품목은 전부 사이트 갤러리에 있다 | 재고 목록과 갤러리의 대사, 미등록 품목 입고 차단 | 신규 품목은 보정 전까지 그 품목 주문 전건 재검수. 미인증 범주가 생기면 전면 중단 |

## 5. 감사 추적 로그

```json
{
  "seq": 17, "prevHash": "…", "logicalTime": 18, "recordHash": "…",
  "kind": "DECISION", "actor": "KERNEL",
  "case": "HMAC-SHA256(key, case_id)[:24]", "inputSha": "SHA-256(canonical(kernel input))",
  "versions": {"ruleset": "…", "gallery": "…", "gate": "…", "calib": "…"},
  "params": {"alpha": 0.01, "deltaCat": 0.003846, "tauMargin": 0.0625, "tauImprintQ": 2, "gammaImprintQ": 3},
  "gateA": "PASS|FAIL", "gateB": "PASS|FAIL", "decision": "AUTO_PASS|REVIEW", "reasons": ["IDENT:IMPRINT_MISMATCH"]
}
{
  "kind": "REVIEW_ACTION", "actor": "<약사 가명>", "ref": 17,
  "action": "CONFIRM_HOLD|RELEASE_AFTER_VERIFY", "reasonCode": "HOLD_CONFIRMED|OVR_VISUAL_VERIFIED", "…": "seq/prevHash/logicalTime/recordHash"
}
```

- `recordHash = SHA-256(canonical(record \ {recordHash}))`, `prevHash`는 직전 `recordHash`, 첫 기록은 `SHA-256("dispense18:genesis:" + ledger_id)`.
- `logicalTime`은 엄격 증가하는 정수 논리 시계다. 벽시계는 신뢰하지 않는다. 기록 추가는 1 이상의 정수 tick만 받고, 검증기도 같은 규약으로 정수가 아닌 시계(불리언 포함)를 `CLOCK`으로 거부한다.
- 환자·주문 식별자는 원문을 넣지 않는다. 키 있는 가명과 입력 해시만 남긴다. 원자료는 접근 통제된 저장소에 두고 해시로 묶는다.
- **Anchor.** $K$건마다 `(seq, recordHash)`를, 종료 시 `(count, head hash)`를 커널이 쓸 수 없는 별도 신뢰 경로(일회 기록 매체, 외부 수신처)에 넘긴다.
- **재생성 금지.** 원장은 `open`으로 열어 사슬·anchor·head를 검증한 뒤에만 이어 쓴다. 기존 원장이 있는데 새로 만들려 하면 `LEDGER_EXISTS`로 거부한다. 기록이 아직 0건인 기존 원장도 기존 원장이다(없음은 `None`, 빈 목록은 있음).
- **신뢰 head는 필수다.** `open`은 별도 신뢰 경로에서 받은 head(정수 건수와 64자리 hex 해시)가 없거나 형식이 다르면 `HEAD_REQUIRED`로 거부한다. 초안은 head를 생략할 수 있었고, 그러면 꼬리가 잘린 원장이 그대로 열렸다(`test_open_requires_a_trusted_head`).
- **표현할 수 없는 입력도 기록한다.** 입력이 정규 JSON으로 표현되지 않으면(NaN, 무한대, JSON 밖의 객체) 표식을 붙인 대체 부호화의 해시로 기록에 묶는다. 초안은 이런 입력에서 입력 해시 계산이 실패해 `LEDGER:COMMIT_FAIL`·`SAFE_HALT`로 끝났다. 판정은 재검수였지만 기록이 남지 않았고 스테이션이 멈췄다. 유효 범위 시험을 추가하다가 발견해 고쳤다(`test_unrepresentable_input_is_still_recorded`).

- **대량 실행.** 본 벤치마크와 시나리오의 10,200건 판정은 기록을 보관하지 않고 같은 사슬의 건수와 head만 남기는 요약 원장(`DigestSink`)에 commit한다. 감사기는 선언형 판정에서 기록을 한 건씩 다시 만들어 이은 사슬의 head가 제출된 head와 같은지 본다. 판정 하나, 사유 하나, 순서 하나가 달라도 head가 달라진다. 운용에서는 보관·anchor되는 원장만 쓴다.

기존 첨부 검토(ATTACHMENT-REVIEW)가 지적한 결함 둘, 곧 시작할 때 로그를 지우고 다시 만드는 것과 신뢰한 최종 해시를 검증하지 않는 것을 이 구조가 막는다. 하네스 결과:

| 시도 | 사슬 내부 검사만 | anchor·head와 함께 |
| --- | --- | --- |
| 기록 한 건의 결정을 수정 | 탐지 (첫 오류 `HASH@1`) | 탐지 |
| 한 건 삭제 / 두 건 순서 교환 | 탐지 (첫 오류 `SEQ@1`) | 탐지 |
| 꼬리 5건 절단 | **통과** (오류 0) | 탐지 (`ANCHOR@63`, `HEAD`) |
| 한 건을 고치고 사슬 전체 해시를 다시 계산 | **통과** (오류 0) | 탐지 (`ANCHOR@15`), `open` 거부 |
| 기존 원장 위에 새 원장 생성 | — | 거부 (`LEDGER_EXISTS`) |

가운데 열의 "통과" 두 칸이 anchor를 두는 이유다. 감사기는 이 맹점이 근거 파일에 그대로 적혀 있는지도 확인한다. 수정·삭제·교환은 anchor와 함께 검증했고, 표의 "첫 오류"가 사슬 내부 검사 코드라는 점에서 내부 검사만으로도 걸린다는 것을 읽을 수 있다.

## 6. 검증 하네스와 테스트 스위트

### 6.1 구조

| 계층 | 파일 | 역할 |
| --- | --- | --- |
| 고정 입력 | `fixtures/*.json` 6개, 잠금 SHA-256 | 갤러리 36품목(+갤러리 밖 6), 규칙표, 게이트 구성, 규칙 격자, 경계 사례와 기대값 |
| 자료 계층 | `_contract.py`, `_synth.py` | 잠금·정규 JSON, counter 기반 난수로 만든 합성 비전 출력과 주문. 판정 논리 없음. 솔버와 감사기가 공유 |
| 솔버 | `matcher.py`, `rules.py`, `conformal.py`, `kernel.py`, `ledger.py`, `bench.py` | 참조 구현 |
| 감사기 | `audit.py` | 솔버 모듈을 import하지 않고 다른 구현으로 재계산, 관문 G1–G4 판정 |

감사기의 대체 구현: 하향식 메모이즈 편집거리, 가능세계 오라클(유리수), 정확 유리수 이항 꼬리, 선언형 게이트(사례별 결정·사유 전체의 digest 대조), 고장표, 별도 해시 사슬 검증기. 독립성의 범위는 **코드 경로**다. 합성 자료를 만드는 `_synth.py`는 공유하며, 같은 사람·같은 프로세스가 정답을 볼 수 없는 블라인드 환경은 아니다.

### 6.2 경계 사례 66건 (기대값은 설계 규칙에서 손으로 작성해 `boundary_expected.json`에 고정)

| ID | 조작 | 기대 |
| --- | --- | --- |
| BC01 | 무잡음 정상 촬영, 비LASA 품목 | AUTO_PASS |
| BC02 | lockout 품목의 정상 촬영 | REVIEW: IMPRINT_MISMATCH, LOCKED, SET_MULTI, LOW_MARGIN |
| BC03 / BC04 | 가림 비율 = θ / θ 초과 | AUTO_PASS / OCCLUDED |
| BC05 / BC06 | 반사율 = θ / θ 초과 | AUTO_PASS / GLARE |
| BC07 / BC08 | 수량 추정기 하나만 불일치 / 둘 다 처방 수량 미만 | COUNT_MISMATCH |
| BC09 | 처방 수량 0 | ORDER:PARSE_ERROR |
| BC10 / BC11 | 혼동 문자 1개 / 2개(거리 = τ, gap = γ 동시 경계) | AUTO_PASS |
| BC12 / BC13 | 혼동 문자 + 삭제 / 혼동군 밖 치환 1개 | IMPRINT_MISMATCH |
| BC14 | 앞·뒤 면 뒤바뀜 | AUTO_PASS |
| BC15 / BC16 | OCR 신뢰도 = 하한 / 하한 미만 | AUTO_PASS / LOW_CONF |
| BC17 | 색상 속성 불일치 | ATTR_MISMATCH |
| BC18 | 유사 외형 짝이 트레이에 있음 | IMPRINT_MISMATCH, SET_OTHER, LOW_MARGIN |
| BC19 / BC20 | margin = τ_m / τ_m 미만 | AUTO_PASS / LOW_MARGIN |
| BC21 / BC22 | 점수에 비수치 / 점수 벡터 길이 부족 | CONFORMAL:EXCEPTION |
| BC23 | 어느 품목도 conform하지 않음 | SET_EMPTY |
| BC24 | 다른 stratum 품목이 트레이에 있음 | IMPRINT_MISMATCH, ATTR_MISMATCH, SET_OTHER, LOW_MARGIN |
| BC25 / BC26 | 한 범주 보정 크기 n_min − 1 / n_min | UNCERTIFIED / AUTO_PASS |
| BC27 | drift latch 켜짐 | MONITOR:DRIFT_LATCH |
| BC28 | 규칙표 SHA 불일치 | INTEGRITY:SHA_MISMATCH |
| BC29 / BC30 | 보정표 경과일 = 한도 / 한도 + 1 | AUTO_PASS / CALIB_STALE |
| BC31 | 감사 기록 commit 실패 | REVIEW, 상태 SAFE_HALT |
| BC32 | 기대 품목이 갤러리에 없음 | UNMAPPED_EXPECTED, RULES:UNKNOWN, IDENT:EXCEPTION, CONFORMAL:EXCEPTION |
| BC33 / BC34 / BC35 | 연령 = 하한 / 하한 − 1 / 결측 (연령금기 성분) | AUTO_PASS / FLAG / UNKNOWN |
| BC36 / BC37 | 연령 = 노인 기준 / 기준 − 1 | FLAG / AUTO_PASS |
| BC38 / BC39 / BC40 | 검사 경과 91일 / 90일·eGFR 낮음 / 90일·eGFR 높음 | UNKNOWN / FLAG / AUTO_PASS |
| BC41 / BC42 | 병용금기 구간이 하루 맞닿음 / 하루 떨어짐 | FLAG / AUTO_PASS |
| BC43 / BC44 / BC45 / BC46 | 일일량 = 상한(MG) / 같은 양의 MCG 표기 / 같은 수를 G로 표기(1,000배) / 미지원 단위 | AUTO_PASS / AUTO_PASS / FLAG / UNKNOWN |
| BC47 / BC48 / BC49 | 기간 = 한도 / 한도 + 1 / 종료일 결측 | AUTO_PASS / FLAG / UNKNOWN |
| BC50 | 분할 불가 품목의 0.5 단위 | FLAG |
| BC51 / BC52 | 임부금기 성분에 임신 여부 결측(여) / 남성 | UNKNOWN / AUTO_PASS |
| BC53 / BC54 | 둘째 줄 미매핑 / 동일성분 중복 | UNKNOWN / FLAG |
| BC55 | 인증된 유사 외형 품목의 정상 촬영 | AUTO_PASS |
| BC56 | 갤러리 밖 근접 품목 | IMPRINT_MISMATCH (Gate B는 통과함) |
| BC57 | 1등과 유사 품목들의 점수 차가 작음 | SET_MULTI, LOW_MARGIN |
| BC58 / BC59 | 가림 비율 음수 / 반사율 1 초과 | CAPTURE:EXCEPTION |
| BC60 | 수량 추정값 음수 | CAPTURE:EXCEPTION |
| BC61 / BC62 | OCR 신뢰도가 불리언 / 속성 신뢰도 1 초과 | IDENT:EXCEPTION |
| BC63 / BC64 | 점수 1 초과 / −1 미만 | CONFORMAL:EXCEPTION |
| BC65 | 주문 줄의 종료일이 시작일보다 앞섬 | RULES:EXCEPTION |
| BC66 | 신뢰도 = 1 (상한 포함) | AUTO_PASS |

### 6.3 단위 시험 61개 (`tests/test_dispense18.py`)

커밋된 근거와 재실행의 바이트 동일성, 솔버 함수를 막은 상태에서의 감사 통과, 변조된 근거의 관문별 거부(G1 해시, G2 lockout 목록, G3 임계치), 입력 잠금, 편집거리 비용과 면 순서 불변, 자모 분해, 혼동 그래프, 배치 샘플러, ArcFace 기준 logit, Kleene OR, CKD-EPI 기준점과 임계 전환, 십진 경계, MIMIC 행 정규화, 순서통계량과 정확 이항의 일치, $n_{min}$, 배제 경계(θ와 같으면 남고 바로 아래면 빠짐), martingale, 이항 상한, 단계 계약의 fail-closed(형식 위반 10종), commit 반환값 4종, 쓰레기 입력 8종, 단일 필드 오염 12종, **보정에 쓰지 않은 새 촬영 2,000장**에서의 비포함률, 원장 변조·재생성, 원장 없는 커널의 release 불가, 대량 사슬 head 위조의 G3 거부.

교차검증 뒤 추가한 것: 온라인 감시가 촬영을 먼저 채점하고 나중에 기준에 넣는지, 감시 객체가 기준 상태를 소유하는지, 온라인 순위의 균등·독립과 고정표 순위의 의존(24개 순서 전수), 새 촬영 1,200장에서 무경보·잡음 2배 촬영에서 latch, 범위 밖 비전 값 27종의 사유별 거부와 경계 포함값 7종의 통과, 표현할 수 없는 입력의 기록, 역전 구간과 불리언·누락 일자의 거부, 수치 표기의 정확 왕복 11종, creatinine 타임스탬프 규칙 22종(같은 날 나중 값, 기준 시각과 같은 값, 올림, 최신 행 불가, 동시각 상충, 날짜만 있는 행, 해석 불가·시간대 표기), 신뢰 head 없는 `open` 거부 10종.

재검토 2차 뒤 추가한 것: 기록 0건인 기존 원장의 재생성 거부와 정수 tick, martingale 지수 격자의 $(0,1]$ 검증, 형식이 어긋난 타임스탬프의 거부(앞 열 글자로 읽지 않음), 28자리를 넘는 용량 곱의 정확 비교와 120자리를 넘는 곱의 UNKNOWN, 소수 연령·연도의 UNKNOWN.

## 7. 합성 하네스 실측

합성 갤러리와 시뮬레이터에서 나온 수치다. 실제 낱알 사진이나 실제 처방에서의 성능이 아니며, 과제 목표 충족을 뜻하지 않는다. 구성: $\alpha=0.01$, $\delta=0.05$, 범주 13개($\delta_c=0.003846$, $n_{min}=554$), 범주별 보정 640–642장($k=1$), $\tau_m=0.0625$.

| 분모 | N | 자동 통과(= 미탐) | 단측 95% 상한 | Gate A 단독 통과 | Gate B 단독 통과 |
| --- | ---: | ---: | ---: | ---: | ---: |
| LASA 교체 | 1,500 | 0 | 0.1995% | 0 | 0 |
| 임의 교체 | 1,000 | 0 | 0.2991% | 0 | 0 |
| 갤러리 밖 | 600 | 0 | 0.4980% | 0 | 220 |
| 수량 오류 | 400 | 0 | 0.7461% | 0 | 360 |
| 규칙 FLAG 주문 | 600 | 0 | 0.4980% | 0 | 596 |
| 판단불가 주문 | 400 | 0 | 0.7461% | 0 | 398 |
| 액제(분모 없음) | 0 | — | HOLD | — | — |

| 재검수 부담 (정상 조제 3,000건) | 건수 | 자동 통과 |
| --- | ---: | ---: |
| 전체 | 3,000 | 1,859 (62.0%) |
| stratum 범주 품목 | 2,470 | 1,742 (70.5%) |
| 품목 범주(인증된 유사 외형) | 220 | 117 (53.2%) |
| lockout 품목 | 310 | 0 (0%) |

- 전 7,500건 중 재검수 5,641건, 자동 통과 1,859건. 사유 집계(한 건에 여러 사유 가능): `IMPRINT_MISMATCH` 3,765, `LOW_MARGIN` 3,231, `SET_OTHER` 2,339, `ATTR_MISMATCH` 2,028, `LOW_CONF` 1,729, `LOCKED` 1,198, `SET_MULTI` 694, `COUNT_MISMATCH` 686, `OCCLUDED` 590, `GLARE` 553, `SET_EMPTY` 81, `RULES:FLAG` 600, `RULES:UNKNOWN` 400.
- 참 라벨 비포함률: 품목 범주 3/1,604, stratum 범주 3/4,241. 둘 다 $\alpha$ 아래.
- 미인증 시나리오(한 범주 $n=553$): 900건 전부 `CONFORMAL:UNCERTIFIED`, 자동 통과 0.
- drift 시나리오(온라인 감시): 300번째부터 잡음 2배이면 342번째에 latch, latch 이후 557건 자동 통과 0, latch 전 43건 중 자동 통과 22건·오조제 자동 통과 0건(오조제 6건). 잡음 1.25배이면 796번째에 latch, latch 이후 103건 자동 통과 0, latch 전 497건 중 자동 통과 253건·오조제 자동 통과 0건(오조제 92건).
- 본 벤치마크의 감시 martingale: 5,845회 갱신, 최대 $\ln M_t=0$(경보선 4.61), 경보 없음.
- 대량 판정 10,200건(본 벤치마크 7,500 + 미인증 900 + drift 두 시나리오 1,800)이 전부 사슬에 commit됐고 감사기가 다시 만든 사슬의 head와 같다.
- 규칙 격자 22,410건 전수 일치(전 규칙 CLEAR 7,867건), 단조성 27,000쌍 위반 0. 파이프라인 50,400조합 중 자동 통과 1.
- 실행 시간: 솔버 약 2초, 감사 약 2.5초, 단위 시험 61개 약 10초(표준 라이브러리 Python).

재현:

```sh
python3 -B -m dispense18.orchestrate --output evidence/dispense18-run.json
python3 -B -m dispense18.audit --run evidence/dispense18-run.json --output evidence/dispense18-audit.json
python3 -B -m unittest discover -s tests -p test_dispense18.py -v
```

## 8. 위험통제 추적표 (ISO 14971 ↔ 요구사항 ↔ 시험)

| 위해 요인 | 위험통제 | 요구사항 | 시험 |
| --- | --- | --- | --- |
| H1 유사 외형 품목 오조제가 무경보로 통과 | 각인 유일성 + conformal 배제의 이중 장벽, lockout | REQ-ID-01 gap ≥ γ, REQ-CP-01 $C(x)=\{E\}$, REQ-LK-01 | BC02, BC10–14, BC18–20, BC55, BC57, LASA 1,500건, `test_gate_b_holds_on_captures_it_has_never_seen` |
| H2 수량 오류 | 독립 추정기 두 개의 일치 | REQ-CA-01 $N_A=N_B=q$ | BC07–09, 수량 오류 400건 |
| H3 금기·주의 주문 미표시 | 결정론 규칙 엔진, 범위 안 전수 대조 | REQ-RU-01 Kleene 판정, REQ-RU-02 오라클 일치 | BC33–54, 격자 22,410건, 규칙 FLAG 600건 |
| H4 결측·오래된 자료를 안전으로 취급 | UNKNOWN은 재검수, 기본값 금지, 단조성 | REQ-RU-03 정보 단조성 | BC35, BC38, BC46, BC49, BC51, BC53, 27,000쌍 |
| H5 단위 혼동(1,000배) | 허용 단위만 환산, 십진 산술 | REQ-RU-04 | BC43–46, `test_exact_decimal_dose_boundaries` |
| H6 촬영 품질 저하 | 가림·반사 임계 초과 시 강제 재검수 | REQ-CA-02 | BC03–06 |
| H7 모델·카메라 drift | 온라인 conformal martingale latch(품목별 기준, 감시 객체가 상태 소유), 선행 감시·주기적 재보정 | REQ-MO-01 | BC27, drift 시나리오 2종(잡음 2배·1.25배), `test_online_ranks_are_independent_and_uniform_but_fixed_table_ranks_are_not`, `test_monitor_is_quiet_on_fresh_captures_and_latches_on_noisier_ones` |
| H8 보정 부족 | $n_{min}$ 미만 범주는 미인증, 전면 중단 | REQ-CP-02 | BC25–26, 미인증 시나리오 |
| H9 소프트웨어 고장·지연 | 단계 계약, 예외·시간 초과·형식 위반은 재검수. 실제 시간은 커널 밖 watchdog이 재고, 결정 기록이 없는 건은 하류에서 재검수 | REQ-FS-01, REQ-FS-02 watchdog, REQ-FS-03 무응답 ≠ 통과 | 50,400조합(보고된 tick 기준), `test_stage_contract_is_fail_closed`, BC21–22, BC32. REQ-FS-02·03은 통합 단계 시험 대상이며 하네스 범위 밖 |
| H10 구성 불일치 | 규칙표·갤러리·게이트 SHA 고정, 보정 유효기간 | REQ-IN-01 | BC28–30 |
| H11 감사 기록 유실·변조 | commit 없이는 release 없음(원장 미연결 포함), hash chain + anchor, 재생성 금지 | REQ-LG-01–03, REQ-LG-04 신뢰 head 필수 | BC31, 변조 시도 9종, `test_kernel_without_a_trail_cannot_release`, `test_open_requires_a_trusted_head`, `test_unrepresentable_input_is_still_recorded`, 대량 사슬 10,200건 head 대조 |
| H12 갤러리 밖 품목 | 각인·속성 장벽, 재고 대사(비전 밖) | REQ-OS-01 | BC56, 갤러리 밖 600건 |
| H14 범위 밖 비전 출력이 임계 비교를 통과(음수 비율, 불리언·무한대 신뢰도, 범위 밖 점수) | 비교에 앞선 유효 범위 검사, 위반은 예외 → 재검수 | REQ-IV-01 | BC58–64, BC66, `test_out_of_range_vision_values_are_rejected_before_any_comparison` |
| H15 기준 시각 이후의 검사값, 역전된 투여 구간, 정밀도를 잃은 수치가 판정에 쓰임 | 전체 타임스탬프 비교, 최신 행을 못 쓰면 판단불가, 역전 구간 거부, 정확 왕복 수치만 채택 | REQ-RU-05 | BC65, `test_creatinine_is_the_latest_row_at_or_before_the_reference_timestamp`, `test_reversed_interval_is_rejected_not_cleared`, `test_mimic_rows_normalise_conservatively` |
| H13 경보 피로로 인한 재검수 형해화 | 재검수율을 미탐 상한과 함께 보고, 사유 코드별 집계 | REQ-RP-01 | §7의 부담 표 |

## 9. 다음 관문

1. **실제 영상으로 V1–V3 교체.** 평가 계획이 고정한 ePillID 분할(학습 2,238 / 검증 745 / 시험 745)에서 인코더를 학습하고, 검증 분할로 범주별 보정 집합을 만든 뒤 시험 분할에서 §4.6의 표를 채운다. 커널·감사기·시험은 그대로 쓴다. 바뀌는 것은 `_synth.py`의 관측 생성부뿐이다. ePillID 소비자 사진은 품목당 평균 약 3.9장(3,728장 / 960종)이어서 품목 범주는 $n_{min}=554$에 닿지 않는다. 인증 가능성이 있는 것은 stratum 범주뿐이고, 닿지 않는 범주가 남는 한 결과는 "미인증, 자동 통과 0, 재검수 100%"로 보고한다. 이 수치가 실제 인증에 필요한 보정 촬영 규모(유사 외형 품목당 554장 이상)를 정한다.
2. **식약처 낱알식별 API**(인증키 필요)로 국내 갤러리와 혼동 그래프를 생성하고 lockout 품목 수를 집계한다. 이 수치가 국내 품목에서의 재검수 부담 하한이다.
3. **규칙표를 공개 DUR 규칙 코드에서 생성**하고 각 규칙에 원천 코드·고시 버전을 붙인다. 격자 전수 대조는 규칙표가 바뀔 때마다 다시 돈다.
4. **주문 감사 분모.** MIMIC-IV에는 처방 원문·조제 실물·독립 오류 판정을 한 사건으로 묶은 정답이 없다. 주문 감사의 양성은 규칙표로 정의한 주입 사례이며, 실제 오류율 추정에는 쓰지 않는다.
5. 격리 환경 재실행과 감사자 서명은 이 문서의 범위 밖이다.
