# 판정 위반 게이트 — 설계안 (2026-09-20, 구현 전 / Codex 의논용)

작성: Claude. 상태 **설계안**. 코드 변경 0, 유료 호출 0, DB 변경 0.
대상 주행: project `8f8a58f6-cc7d-4143-940a-1c9c6ab707f7` /
episode `a28fb896-b811-463c-8bcc-ba732968bd7c` (선택 샷 239).

## 1. 무엇이 문제인가 — 실측

판정기는 후보마다 `hard_violations` 를 적는다. 팩(`prompts/_base/multiroll_judge/
16.202608280010/judge_still.md:51-58`)은 이 칸을 이렇게 규정한다:

> `hard_violations` is for disqualifying faults only — things that make
> the frame unusable.

즉 **여기 적힌 것은 정의상 「그 컷을 못 쓰게 만드는 결함」**이다.

★**정정(Codex 리뷰 수용).** 이 문서의 첫 판은 「프로덕션 소비 0」이라고
적었는데 **틀렸다.** `multiroll_gemini.py:659-680` 이 두 슬롯의 위반을
합집합으로 모아 **후보당 이진 페널티**(`SELECT_VIOLATION_PENALTY = 0.25`)로
쓰고, :723 이 다시 구조 기록으로 낸다. 없는 것은 **소비**가 아니라
**실격 강제**다.

선정(`run_multiroll_select`)은 위반을 0.25 깎은 뒤에도 **둘 중 나은 것**을
고른다. 둘 다 실격이면 실격본이 `_sel` 이 되어 하류로 나간다.

### 이미 산 기록 전수 (records.json, 새 유료 호출 0)

`projects/…/scene/recipe/records.json`, base 레코드 239개:

| | |
|---|---|
| 이긴 롤에 `hard_violations` 가 달린 샷 | **81 / 239 (34%)** |
| 그 위반 문장 수 | 103 |
| `needs_reshoot` 가 선 샷 | 21 (`all_candidates_fail` 경로) |
| 둘의 교집합 | 19 |
| **위반이 있는데 아무 표시도 없는 샷** | **62** |
| `critique_skipped` | 239 / 239 (관문 꺼짐) |
| 롤 라벨 | A·B 두 장. 관찰자는 `[gemini-pro]` + `[gpt-high]` 둘 |

`::cine` 239 · `::signage` 239 · `::bgfirst_bg` 93 에는 `readings` 가
**0건**이다 — 이 게이트의 사정거리는 **조립(base) 단계뿐**이다.
변환(cine) 검증은 별개 관문이고 지금 범위 밖이다.

### 위반 103문장을 눈으로 갈라 본 분포 (설계 근거용, 코드가 하는 일 아님)

| 부류 | 대략 수 | 예 |
|---|---|---|
| 물리·해부 불가능 | ~21 | S10sh3 손가락 융합 · S34sh3/S73sh11 세 번째 손 · S31sh8 투명 의자 · S66sh14 허공의 손전등 · S4sh1 불가능한 룸미러 반사 · S50sh4 통째로 2D 일러스트 |
| 허용 밖 인물 등장 | ~22 | S68sh3 「사람 없음」인데 오토바이 탑승자 셋 · S85sh23 배경 인물 |
| 판독 가능한 글자·로고 | ~20 | 찰리 가슴 `Ubik` · 간판 한글 |
| 장소 참조 밖 물체·구조 변경 | ~12 | S35sh3 위성접시 · S90sh4 작업대 증설 |
| 지시된 배치·시점 불일치 | ~12 | S57sh3 철창 안 시점인데 바깥에서 |
| 앞 컷 얼굴 복사·신원 | ~5 | S26sh7 민병대 4명 복사 |

★**이 줄은 철회한다(§9).** 첫 판은 「81건 거의 전부가 못 쓰는 컷이 맞다」고 적었는데,
그건 **글만 읽고 그림을 안 본** 판단이었다. 게다가 위 표의 「판독 가능한
글자·로고」 ~20건은 **사용자 기준으로 결함이 아니다** — §9 를 보라.

## 2. 설계 — 게이트를 어디에 어떻게 놓는가

### 2.1 판정은 새로 사지 않는다

게이트의 입력은 **이미 산 판정의 `readings[selected].hard_violations`** 다.
검출 비용 0. 재롤의 값은 §7.1② 를 보라(이미지 1 + VLM 2).

### 2.2 자리

`run_multiroll_select` 의 선정 직후, `_sel` 을 물질화하고
`critique_enabled` 분기로 가기 **전**. 즉 결함 검사(critique) 관문과
**독립**이다 — 사용자가 시간 때문에 끈 관문(`STILL_RECIPE_CRITIQUE_ENABLED`,
샷당 7~9콜 × 118초)을 켜지 않는다.

게이트 한 샷의 추가 유료:
- 재생성 이미지 **1장**
- 재판정 **1회** (기존 승자 vs 재생성본)

★이 비교는 철회한다 — 참값은 §7.1② 다.

### 2.3 처방은 이미 있는 배선을 쓴다

- `build_regen_prompt(issues, base_prompt, regen_head, regen_tail,
  faults_header)` — 원 브리프 + 「무엇이 틀렸나」 목록.
  `issues` 는 `{issue_ko, fix_en}` 이므로 위반 문자열을 `issue_ko` 로
  실으면 그대로 붙는다.
- `repair_method="regenerate"` · `regen_gen_fn` (= 롤 생성기) —
  still_recipe.py:1146-1157 에 이미 연결돼 있다.
- 재판정은 `fix_rejudge_fn` / `_set_needs_reshoot` 의 기존 계약.

즉 **새 프롬프트 팩도, 새 판정 모델도 필요 없다.**

### 2.4 상한

- 게이트 재롤 **샷당 1회**(canonical 샷당, 재개까지 합산 — §7.5).
- ★「둘 중 나은 것 + `needs_reshoot=True`」는 **철회했다.** 종착은 §7.4 가
  현행이다.
- 에피소드 단위 상한(예: 게이트가 건드릴 샷 수 상한)을 env 로 둘지 → 쟁점 4.

### 2.5 무엇을 걸 것인가 — 두 단계

**1단계 (스키마 변경 없음, 기존 기록으로 바로 가능)**
> 이긴 롤의 `hard_violations` 가 **비어 있지 않으면** 받지 않는다.

이것은 배열 길이만 보는 **구조 판단**이다. 글자를 읽지 않으므로
[[feedback-no-literal-substring-meaning]] 에 걸리지 않는다.
걸리는 범위 = 실측 81/239.

**2단계 (스키마에 종류 칸 추가)**
`build_judge_schema` 의
```python
"hard_violations": {"type": "array", "items": {"type": "string"}}
```
를 객체 배열로 바꾼다:
```python
{"type": "array", "items": {"type": "object", "properties": {
    "kind": {"type": "string", "enum": [...]},   # 구조 칸
    "text": {"type": "string"}}}}
```
`kind` 후보(작품 무관·일반):
`impossible_anatomy` · `unsupported_floating` · `duplicated_subject` ·
`impossible_optics` · `wrong_medium` · `identity_reuse` ·
`unlisted_person` · `unlisted_object` · `framing_mismatch` ·
`readable_text_or_logo` · `other`

종류를 **모델이 구조 칸으로** 고르고 **코드는 enum 값만** 본다.
이러면 ① 축마다 처방·상한을 달리 줄 수 있고 ② 판정기마다 「hard」의
해석이 흔들리는 문제(팩 :57 이 적은 139건 대 17건)를 코드가 흡수한다.

★1단계와 2단계는 **같은 게이트의 정밀도 차이**일 뿐 배선은 같다.

## 3. 지문·재개 — 여기가 제일 비싼 결정

`run_multiroll_select` 는 `compute_input_fingerprint` 로 산출 재사용을
판정한다. 게이트 정책을 `_fp_extra` 에 접으면 **239샷 전부 지문 불일치 →
`_clear_outputs` → 전량 재생성**이다.

- **접는다(권장)**: 게이트는 최종 산출을 바꾸는 실질 입력이다. 안 접으면
  「반대 정책으로 만든 산출이 같은 조건으로 기록」되는 기존 사고 부류를
  되풀이한다(`fix_ref_contract_sha` 주석이 같은 이야기를 한다).
  ★「추가 45샷」은 **철회한다** — 194/239 는 확정 범위가 아니다(§7.7).
- 안 접고 재개 갈래에서 옛 record 를 소급으로 읽는 길도 있으나,
  그 record 에는 종류 칸이 없어 1단계 정밀도로만 돌고, 지문이 같은 채
  산출만 달라져 기록이 어긋난다.

## 4. Codex 에게 묻는 쟁점

1. **자리**: 게이트를 `run_multiroll_select` 안(선정 직후)에 두는 것이
   맞나, 아니면 호출자(still_recipe)가 record 를 보고 되부르는 편이 나은가.
   전자는 `_sel` 물질화·persist 순서와 얽히고, 후자는 「같은 규칙 두 곳」
   위험이 있다.
2. **1단계로 먼저 갈 것인가, 2단계까지 한 번에 갈 것인가.**
   1단계는 81/239 를 전부 재롤한다(팩 정의상 전부 실격이므로 근거는 있다).
   2단계는 스키마 변경 = 판정 계약 변경이라 어차피 전량 지문이 움직인다.
   → 어차피 지문이 움직인다면 **2단계로 한 번에** 가는 편이 싸 보인다. 동의하나?
3. **재롤 실패 시**: 둘 중 나은 것 + `needs_reshoot=True` 로 받는 것이
   맞나. 아니면 `all_candidates_fail` 처럼 별도 칸을 새로 둘 것인가.
   지금 `needs_reshoot` 는 **기록만 되고 소비처가 0**이다(갤러리·Opik 뿐).
4. **상한**: 샷당 1회 + 에피소드 상한(env)이 필요한가, 샷당 1회로 충분한가.
   239샷 중 81이 걸리면 이미지 81장 + 판정 81회가 한 판에 붙는다.
5. **중복 지출 위험**: 게이트 재롤이 `_mark_spend_attempt_once` 와
   `_stamp_shot_run(produced=…)` 계보에 어떻게 들어가야 하나.
   재롤본이 지면 최종 산출의 저자는 여전히 초기 롤이다.
6. **cine·signage 는 범위 밖**으로 두는 것이 맞나 (readings 0건).

## 5. 이 문서가 승인하는 것 / 안 하는 것

- 승인 대상 아님: 구현, 유료 재생성, DB 수리, 관문 켜기.
- 다음 걸음: Codex 의견 → 사용자 결정 → 구현 → Codex diff 리뷰.

---

## 6. 보충 조사 (Codex 검토 중에 더 판 것)

### 6.1 선정은 이미 위반을 점수로 깎고 있다 — 고칠 곳은 선정이 아니다

`multiroll_gemini.py:46` `SELECT_VIOLATION_PENALTY = 0.25`, :679 에서
「그 후보에 하드 위반이 하나라도 있으면 한 번 깎는다」(말수로 안 깎는다).
그리고 :660-663 이 두 슬롯(gemini-pro · 둘째 관찰자)의 위반을
**합집합**으로 모아 `[{model}] …` provenance 를 달아 둔다.

실측으로 그 페널티가 잘 듣는지 봤다 (239샷):

| | 샷 |
|---|---|
| **두 후보 다 위반** | **79** |
| 이긴 쪽만 위반 | **2** |
| 진 쪽만 위반 | 108 |
| 둘 다 깨끗 | 50 |

→ **선정의 오선택은 2샷뿐이다.** 「이긴 롤에 위반이 달려 나간다」의 실체는
**둘 다 나쁜 79샷**이고, 이건 둘 중 고르는 일로는 원리상 해결되지 않는다.
후보를 더 뽑는 수밖에 없다. **선정 로직·페널티는 건드리지 않는다.**

### 6.2 이 게이트는 코드가 이미 예고한 후속이다 — 전제도 적혀 있다

`multiroll_gemini.py:676-678` 주석:

> 이건 「품질 개선」이 아니라 기록·계산 정합 수정이다. **「하드면 무조건
> 탈락」으로 바꾸는 것은 false-positive pilot 뒤 별도 판단이다.**

★내 설계에 이 전제가 빠져 있었다. 81샷이 걸리는데 **그중 몇 건이 오검출인지
아무도 안 쟀다.** 팩 자신이 :57 에서 「한 심판이 minor 139건을 hard 로
올렸다」고 적는다. 나는 위반 103문장을 **글로만** 읽고 「거의 전부 실격이
맞다」고 봤는데, 그건 그림을 안 본 판단이다.

**그래서 게이트를 켜기 전 단계를 하나 넣는다 — 유료 0:**
걸리는 81샷의 최종 `_sel` 그림과 그 샷의 위반 문장을 나란히 놓은 갤러리를
만들어 **사람이 본다.** 오검출 비율이 나오면 그때 1단계로 갈지 2단계
(종류 칸)까지 갈지, 어느 종류를 걸지가 수치로 정해진다.

### 6.3 상한의 관례는 이미 있다

`config.py:908` `still_jit_regen_limit = 64` — 「완료 샷 재생성이 이 수에
닿으면 멈추고 사람 확인을 요구한다(256샷의 25%)」. 게이트 상한도 같은 꼴로
둔다(쟁점 ④).

### 6.4 처방 배선은 켜져 있지 않을 뿐 이미 기본값이다

`config.py:899` `still_repair_method = "regenerate"`(사용자 확정)인데,
:874 `still_recipe_critique_enabled = False` 가 master 라 **읽히지 않는다.**
게이트는 이 master 와 **독립된 자기 스위치**를 가져야 한다 — 그래야
사용자가 시간 때문에 끈 관문을 켜지 않고도 돈다.
★단 `still_repair_method` 의 배선 범위는 **표준 갈래 하나**다
(config.py:895-903: confined·bgfirst·variants 는 아직 edit, ab_active 는
구조상 제외). 게이트의 사정거리도 같은 제한을 받는지 확인이 필요하다.

---

# 7. Codex 설계 리뷰 반영 (2026-09-20, BLOCK 3건)

리뷰 주장을 file:line 으로 하나씩 열어 확인했다. **전부 맞다.** 아래는
확인 결과와 그에 따른 설계 개정이다.

## 7.1 값이 틀렸던 것 두 가지

### ① 게이트가 실제로 덮는 범위는 81이 아니라 **43**이다

재생성·재판정 배선(`repair_method` · `still_regen_texts`)은
`still_recipe_service.py:5161-5176` 의 **표준 갈래 한 곳에만** 전달된다.
그 주석이 이유를 적어 둔다 — confined 는 도면 기하가 권위, bgfirst 는
배경판이 카메라 권위, ab_active 는 두 파이프가 각각 수리하면 「샷당 1장」이
2장이 된다.

**81샷을 갈래별로 다시 셌다:**

| 갈래 | 샷 | 이긴 롤 위반 | 둘 다 위반 | 이긴 쪽만 |
|---|---:|---:|---:|---:|
| **표준(base)** | 137 | **43** | 43 | 0 |
| bgfirst | 93 | **35** | 34 | 1 |
| confined | 8 | 3 | 2 | 1 |
| conti_ab | 1 | 0 | 0 | 0 |
| 계 | 239 | 81 | 79 | 2 |

→ 「기존 배선을 그대로 재사용」으로 덮이는 것은 **43/81**. 나머지 38은
배선 자체가 없다. 내 첫 문안은 이 점에서 과장이었다.

→ 그리고 **「이긴 쪽만 위반」 2샷은 둘 다 표준 갈래 밖**이다(bgfirst 1 ·
confined 1). 「깨끗한 기존 후보로 바꿔 무료로 푼다」(리뷰 권고)는 방향은
옳지만, **표준 갈래에는 해당이 0건**이다.

### ② 비용은 「이미지 1 + 판정 1」이 아니라 **이미지 1 + VLM 2**다

`_judge_cross_model_order`(`multiroll_gemini.py:462`)는 한 번 부르면
슬롯0(정순) · 슬롯1(역순)을 **2콜 병렬**로 돈다. 재판정 wrapper 1회 =
provider 2회다.

| | 이미지 | VLM |
|---|---:|---:|
| 43샷(표준 갈래) 전부 걸릴 때 | 43 | 86 |
| 81샷(배선을 넓힐 때) | 81 | 162 |

「유료는 그림뿐」·「critique 의 1/4」은 **뺀다**. 생성기의 안전·대체 전송은
여기에 안 들어간 별도다.

## 7.2 확인한 계약 세 가지 (구현이 반드시 지켜야 한다)

- `multiroll_select.py:1083-1084` — `fix_rejudge_fn is None` 이면
  **수정본을 무판정으로 채택**하고 반환한다. 게이트가 이 경로를 그냥 쓰면
  재롤본이 **검증 없이** 최종이 된다. 그런데 `fix_rejudge_fn` 은
  `still_recipe_service.py:1697-1699` 에서 **critique master 가 켜져야**
  만들어진다 → 게이트 활성 조건에서 **따로 만들어 줘야 한다.**
- `_validate_judge_shape`(`:459-496`)는 verdicts·ranking·winner 만 본다 —
  **`readings` 를 아예 안 본다.** 그래서 「이긴 롤의 readings 행이 없음」과
  「위반이 빈 배열」은 **다른 것**이다. 행이 없으면 통과가 아니라 **미검사**로
  다뤄야 한다.
- `record["fix_rejudge"]`(`:1100-1121`)에는 `readings` 가 **없다**
  (cross_model_order·winner·ranking·verdicts·fix_won·all_candidates_fail).
  게이트가 재롤 뒤 위반을 판별하려면 **readings 도 보존**해야 한다.
- 스키마를 `{kind, text}` 객체로 바꾸면 `multiroll_gemini.py:663` 의
  `f"[{model}] {hv}"` 가 객체를 문자열로 **뭉갠다.** 결합·역순 정규화·기록·
  소비를 한 릴리스에서 같이 고쳐야 한다(같은 규칙 두 곳 부류).

## 7.3 갤러리는 판정이 본 그림이 맞다 — 실측으로 닫았다

리뷰가 「최종 SEL 이 나중에 교체됐으면 옛 판정의 증거로 쓰면 안 된다」고
했다. 맞는 우려라 sha 로 전수 대조했다:

```
_sel == 이긴 롤 파일(_a/_b): 238 일치 · 다름 0 · 파일 없음 1
```

**확인 가능한 238건은 바이트 동일**이고, 파일이 없는 1건은 미확인이다.
(그 1건은 갤러리에도 안 들어간다.)
(수동으로 올린 21장은 DB 자산 쪽이고 `_sel` 은 안 건드렸다.)

## 7.4 개정 — 종착 상태를 새로 정한다 (BLOCK 1)

첫 문안은 「재롤해도 안 되면 둘 중 나은 것 + `needs_reshoot=True`」였다.
리뷰 지적대로 그건 **지금의 병을 그대로 재현**한다 — `needs_reshoot` 는
프로덕션 소비처가 0이고, 자산은 `still_recipe_service.py:5731` 에서
`status="generated"` 로 그냥 저장된다.

그래서 게이트는 **세 종착**을 구분해 영속한다:

| 종착 | 뜻 | 최종 산출 |
|---|---|---|
| `clean` | 이긴 후보에 위반 없음 | 정상 |
| `resolved` | 재롤본이 허용 후보로 채택됨 | 정상 |
| `unresolved` | 재롤 뒤에도 허용 후보 없음 | **정상 아님** |

`unresolved` 를 어떻게 다룰지는 **두 갈래이고 사용자 결정 사안**이다:

- **(가) 막는다** — 그 샷의 최종 확정(변환·자산 승격)을 세우고 사람 확인을
  받는다. 다른 성공 샷은 안 건드린다.
- **(나) 내보내되 표시한다** — least-bad 를 최종으로 쓰되 산출·UI 에서
  정상 통과와 **구분되어 보인다**.

어느 쪽이든 `needs_reshoot` 는 **호환용 보조 값**으로만 두고, 실제
소비자는 게이트 종착 칸을 읽는다. 읽는 곳 없이 칸만 늘리지 않는다.

그리고 **허용 여부가 점수보다 먼저다.** 재판정에서 A=위반·B=깨끗인데
점수가 A 가 높다고 A 를 확정하지 않는다.

## 7.5 개정 — 재개·시도 상태 (BLOCK 3)

- `:1542-1564` 의 재개 판단(`sel` 있고 `critique_skipped` 면 즉시 반환)에
  **게이트 완료 여부도 함께** 들어가야 한다. 안 그러면 게이트가 통째로
  우회된다.
- 게이트는 **단계마다** 상태를 남긴다 — 시도함 / 재롤 파일 만듦 / 재판정함 /
  채택함. 「재롤 성공 → 재판정 실패 → 재개」에서 **이미 만든 그림을 다시
  사지 않게** 한다. `unresolved` 도 **종착**이라, 그냥 다시 걸으면 매번
  새 1회를 주지 않는다. 새 장부를 만들지 않고 기존 `records` 의 게이트 칸에 쓴다.
- **「샷당 1회」는 함수 호출당이 아니라 canonical 샷당**이다 — 같은 입력·
  정책에 대한 재개까지 합산한다.
- `_mark_spend_attempt_once`(`:1393-1416`)는 **방문당 유료 구간 진입표**이지
  게이트 재시도 수가 아니다. 게이트 첫 유료 호출 전에 찍되, 중복 구매는
  **별도 게이트 시도 상태**로 막는다.
- `produced` 는 「재롤했나」가 아니라 **최종 bytes 를 누가 만들었나**다.
  옛 SEL 유지 + 재롤 패배 = False · 이번 방문 초기 롤 승리 + 재롤 패배 =
  True · 이번 방문 재롤이 최종 = True. 재개로 옛 재롤 파일을 최종으로 고르면
  **그 파일의 원래 생성자**를 보존한다.
- 진 후보의 구매도 기록에 남기되, **최종 자산의 model·prompt·call id·입력
  sha 를 진 후보에 연결하지 않는다.** 재생성의 입력은 원래 참조이지
  떨어진 후보가 아니다(i2i 가 아니다).

## 7.6 개정 — 1단계 / 2단계 (쟁점 ②)

「어차피 지문이 움직이니 2단계까지 한 번에 가는 게 싸다」는 **철회한다.**
순서는 **무료 육안 대조 → 적용 대상 정의 → 계약 확정**이다.

- 모든 hard 를 막는 계약이면 **1단계(배열 검사)로 충분**하다.
- 일부 종류만 막으려면 typed `kind` 를 **같은 릴리스에서** 넣는다
  (7.2 의 뭉개짐을 같이 고쳐서).
- **옛 문자열 기록에서 enum 을 추측하지 않는다.** 기존 기록과 새 스키마를
  구분한다.
- 종류 칸이 심각도 오판까지 고치는 것은 아니다.

## 7.7 지문 (쟁점 ⑥) — ★**이 절은 철회됐다** (2026-09-20, §15.5~§15.9)

> **아래 결론은 틀렸다.** 「단일 해시에 넣어 롤까지 지우는 것이 맞는
> 동작」이라고 적었는데, 그렇게 하면 **정책을 켜는 것만으로** 대상 샷
> 전부가 지문 불일치가 되어 후보가 지워지고 **롤부터 다시 사진다**.
> 게이트가 하는 일은 「이미 산 후보 중 무엇을 쓰느냐」라 같은 후보를
> 버릴 이유가 없다. 대체안은 **§15.5 · §15.8 · §15.9** 를 보라 —
> 생성 지문에서 정책을 떼고, 정책 신원은 `gate` 가 항상 적고, 캐시
> 반환에는 **호출 0** 으로 소급 적용한다.

(아래는 당시 기록 — 무엇을 어떻게 틀렸는지 남겨 둔다.)

게이트 ON 인 대상만 정책·재롤 한도·스키마 계약·판정/재생성 문안을 접고,
OFF 와 비대상은 **기존 지문 그대로**다. 지금의 단일 해시에 넣으면
`:1527-1539` 가 롤까지 지우는 것이 **맞는 동작**이다 — 무효화 없이 옛 해시를
새 계약인 척 덮어쓰지 않는다.

★**194/239 는 확정 범위가 아니다.** 그것은 앞선 코드·정본 변경으로 잰
값이고, **아직 안 한 팩 수정**은 안 들어 있다. 최종 코드·팩을 고정한 뒤
갈래·연쇄까지 다시 세야 한다. 그리고 **추가 45샷을 구매 45회로 보고하면
안 된다.**

## 8. 지금 닫힌 것 / 안 닫힌 것

**닫혔다**: 문제의 실체(둘 다 위반 79) · 갈래별 적용 범위(43/81) ·
비용의 참값(이미지 1 + VLM 2) · 갤러리 증거의 정당성(sha 동일) ·
지켜야 할 계약 네 가지.

**안 닫혔다 — 유료 적용 전에 닫아야 한다**:
1. **오검출 비율** — 81샷 육안 대조(무료 갤러리 준비됨).
2. **`unresolved` 종착 처리** (가)냐 (나)냐 — 사용자 결정.
3. **적용 범위** — 표준 갈래 43 만이냐, bgfirst·confined 까지 배선을
   넓히느냐 — 사용자 결정(넓히면 설계·구매가 다 늘어난다).
4. 위 셋이 정해진 뒤 최종 팩·코드를 고정하고 **재생성 범위를 다시 계측**.

---

# 9. ★사용자 지적 — 「읽을 수 있는 글씨가 있어도 상관없다」

> "읽을 수 있는 글씨가 있어도 상관 없다고 몇번을 지적했는데.
>  왜 계속 글자 있으면 안된다고 하지?"

**지적이 맞다. 그리고 이 게이트 설계가 그 오검출을 유료 재롤로 태울 뻔했다.**

## 9.1 왜 계속 붙는가 — 실측

`still_recipe.py:2555`:

```python
load_prompt(_MODULE, "no_text" if signage_en else "no_text_none", …)
```

- `signage_en` 이 **있으면** → `no_text.md`: 「저작된 그 문안만 읽히게」
- **없으면** → `no_text_none.md`: **「No readable writing anywhere in this image.」**

그리고 `signage_author` 가 실제로 낸 것:

| | |
|---|---|
| signage 기록 | 239 |
| **문안을 낸 샷** | **1** (S65sh5, 계기판 숫자 `0`) |
| 「읽을 것은 있으나 문안 없음」(cue) | 6 |
| 저작했다가 버려진 것 | 0 |

→ **239샷 중 238샷의 프롬프트 끝에 「아무 글자도 읽히면 안 된다」가 붙었다.**
(records 의 `prompt` 칸 전수 대조: 전면 금지 238 · 정해진 글자만 1)

이 계약은 `still_recipe.py:2549-2553` 주석이 적어 둔 대로
「**승인 목록이 있으면 그 문안만, 없으면 전면 금지**」(2026-08-27 감사 1-A ⑤)다.
막으려던 것은 **모델이 글자를 지어내는 것**이었는데, 막는 방식이
「아무것도 못 읽게」라서 **정본이 그리라고 한 것까지** 같이 막힌다 —
찰리 가슴의 `Ubik` 로고가 그 예다(정본 t2i 에 이미 있다).

## 9.2 그 결과가 판정과 이 설계에 어떻게 새어 들어왔나

판정기는 프롬프트에 적힌 금지를 근거로 위반을 적는다. 그래서 위반 목록에
「가슴의 `Ubik` 문자가 판독된다」·「간판의 한글이 읽힌다」·「지폐 액면 숫자가
읽힌다」 같은 것이 **실격**으로 올라왔다.

내가 81샷 목록을 눈으로 읽어 세어 보니 **글자·로고 관련이 20샷**이다
(S10sh7 · S11sh4 · S13sh10 · S13sh20 · S14sh9 · S15sh4 · S16sh3 · S22sh3 ·
S28sh5 · S29sh6 · S34sh7 · S40sh6 · S41sh22 · S41sh26 · S46sh1 · S46sh17 ·
S51sh14 · S53sh3 · S60sh4 · S64sh24).
★이 목록은 **내가 문장을 읽고 손으로 적은 조사값**이다 — 코드가 글자를
맞춰 분류한 것이 아니고, 코드에 들어가지도 않는다.

**사용자 기준으로 이것들은 결함이 아니다.** 즉 §6.2 가 말한 오검출이
이미 최소 20샷 확인된 셈이고, 그것도 **모델이 잘못 본 것이 아니라
우리 프롬프트가 시킨 것**이다.

## 9.3 그래서 순서가 바뀐다

게이트보다 **글자 절을 먼저 고친다.** 이유:

1. 절이 남아 있는 한 판정기는 같은 위반을 계속 적고, 게이트는 그것을
   유료로 재롤한다.
2. 절을 고치면 프롬프트가 바뀌므로 **어차피 새 판정이 붙는다** — 지금
   기록으로 오검출을 재는 일의 절반이 다시 그려진다.
3. 이 절은 **239샷 전부**에 붙는다. 고치면 지문이 전량 움직인다 —
   재생성 범위를 다시 계측해야 한다(§7.7 과 같은 이야기).

**고칠 자리는 팩이지 코드가 아니다.** `still_recipe.py:2555` 의 갈래는
그대로 두고, `no_text_none` 의 문안을 새 팩 버전으로 올린다
(프롬프트 파일 덮어쓰기 금지 — 새 버전 디렉토리).

어떤 문안으로 갈지는 **사용자 결정**이다:

- **(가)** 「지어내지 마라」만 남긴다 — 화면에 우연히 읽히는 글자는 그냥 둔다.
- **(나)** 절을 통째로 뺀다 — 글자에 대해 아무 말도 하지 않는다.

둘 중 어느 쪽이든 `signage_author` 의 저작 계약(인용 대조)은 건드리지
않는다. 그쪽은 「**지어낸 글자를 승인 목록에 올리지 마라**」는 다른 일이다.

---

# 10. 표기 정책 수리가 끝났다 — 커밋 둘

| 커밋 | 무엇 |
|---|---|
| `a3ddbe6f` | 글자 **전면 금지** 절을 걷는다 (팩 v42 일반 · v43 grok) |
| `3bc3a90c` | **확정된 숫자를 미리 가리던 예외**를 걷는다 (Codex BLOCK 1) |

둘 다 푸시 안 함. Codex 리뷰: 첫 커밋에 BLOCK 1(숫자 가림) → 둘째 커밋으로
수리 → 그 밖에 추가 BLOCK 0.

## 10.1 내가 낸 결함 둘, 둘 다 남이 잡았다

- 첫 문안에 **「이 장소에 원래 있는 것」**을 썼다 — 그게 2026-08-27 에
  배경이 글자를 **지어내던** 통로였다. **옛 래칫(`_LEAKY`)이 잡았다.**
  문안을 「참조가 이미 보여주는 글자」로 좁히고 「그럴듯한 장소라면 있을
  글자를 끌어내지 마라」를 명시로 넣었다.
- v20 문안의 **숫자 가림 조항**을 그대로 옮겨 왔다 — 「정확한 숫자가 그
  샷을 결정하면 그 면을 가려라」는 **필요한 정보일수록 숨기라**는 말이고,
  판정 팩(`judge_still.md:26-30`: 「지폐에는 나라와 액면이 있다. 프롬프트나
  참조가 못박으면 하나씩 확인하라」)과 정면으로 부딪힌다. **Codex 가 잡았다.**
  네 문장을 빼고, 그것을 **강제하던 시험을 반례로 뒤집었다.**

## 10.2 무료 집계 — 고유 산출 단위 (records 사본만 읽음, 새 호출 0)

★운영 생성 함수는 부르지 않았다(`_run_bgfirst_bg`/`_run_groupbg` 는
provider·db.flush·records.save 까지 한다).

| 단위 | 수 | 이번 수리가 닿는가 |
|---|---:|---|
| 샷 base record | 239 | **직접** — 두 `no_text` 갈래가 다 바뀌었다 |
| 공유 배경 `groupbg::*` | 27 | **직접** — `groupbg_tail` |
| 샷별 재투영 `::bgfirst_bg` | 93 | **직접** — `bg_reproject_tail`·`bg_fill_tail` |
| conti A/B 자식 | `ab_conti` 1 · `ab_noconti` 1 | 판정 1 은 이미지가 아니다 |
| `::cine` | 239 (사람이 끈 것 10) | **간접** — SEL bytes 가 바뀌면 |
| 샷당 롤 | 238샷이 2롤 · 1샷이 1롤 | |
| cross-model 슬롯 | 239샷 전부 2 (flip 끈 샷 93) | |

★**Codex 정정을 받아 고쳤다.** conti A/B 샷은 자식 둘이 각각 1롤을
생성하고 **base 는 이긴 자식의 복사본**이라(`still_recipe_service.py:
5102-5110` · `:5128-5130`), base 의 `roll_count=1` 을 그대로 세면 작게
잡힌다.

**정상 경로 예상 이미지**(실패·fallback 제외):

| | |
|---|---:|
| 샷 롤 (238×2 + 자식 1+1) | **478** |
| `bgfirst_bg` | 93 |
| `groupbg` | 27 |
| `cine` (사람이 끈 10 제외) | 229 |
| **계** | **827** |

**예상 VLM**: 후보 선정 480슬롯(238×2 + 자식 둘×2) + A/B outer 2
(`conti_ab.py:162-179` 가 AB/BA 로 두 번) = **482**.
★`judge_flip_skipped` 93건은 **추가 두 번을 막은 것**이지 이 두 슬롯을
0으로 만든 것이 아니다.
★배경 두 생산자는 **이미지 1장·선정 VLM 0** 이다 — `_run_bgfirst_bg`
(`:2043-2078`)·`_run_groupbg`(`:2475-2505`)가 `call_gpt_image_bytes(n=1)`
직결이고 생성 뒤 선정 판정이 없다. 다만 **「배경 선정 VLM 0」을 「배경 관련
유료 호출 0」으로 넓히지 않는다** — moderation 이면 sanitizer + 재이미지,
`groupbg` 시대 조사 캐시가 비면 별도 조사가 붙는다.

## 10.2.1 배경 단위를 records 로 좁혔다 (실측)

Codex 가 준 연결을 따라 **현재 선택 샷이 실제로 소비하는** 것만 셌다
(`base.bgfirst.authority` → `group_key` → `records["groupbg::"+key]`):

| | |
|---|---:|
| `bgfirst` 칸이 있는 샷 | 93 (전부 있음) |
| authority `groupbg` | 28 |
| authority `plate` | 60 |
| authority `seed_bg` | 5 |
| authority `lane_conti_only` | **0** |
| 현재 소비 `group_key` | **27** |
| records 의 `groupbg::*` | 27 — **안 쓰이는 것 0** |

→ **G = 27 확정.** 그리고 `bg_fill_tail`(lane) 갈래는 **이번 판에 0샷**
이다 — 93샷 전부 `bg_reproject_tail` 을 쓴다. (팩은 둘 다 고쳤으니 다음
판에 lane 샷이 생기면 그쪽도 새 정책으로 간다.)

★**이 수에 아직 안 들어간 것**: 팩 수정(찰리 크기·총 서술·옷 층 순서 등)
으로 바뀌는 몫, 재시도·fallback, 그리고 게이트를 켤 경우의 재롤.
★`records` 의 참조는 `<bytes:N>` 표식일 수 있어 **새 지문을 미리 정확히
복원할 수 없다.** 무료로 확정되는 것은 **직접 변경 집합과 의존 연쇄**이고,
실제 구매 수는 **돌린 뒤 대조할 값**이다.
★`shot_run_spend_attempt_count` 는 「유료 구간 **방문 수**」이지 호출 수가
아니다. `_jit_tag_snapshot` 변화 수도 구매 수가 아니다.

## 10.3 게이트 대상이 줄었다 — 다만 짐작이다

글자 지적이 달린 20샷 중 **16샷은 그 지적 하나뿐**이라 게이트 대상에서
빠지고, **4샷**(S40sh6 · S46sh17 · S51sh14 · S60sh4)은 다른 결함이 같이
있어 남는다. **81 → 65.**

★그러나 팩을 고쳤으니 **다음 판정은 새로 붙는다.** 이 수는 옛 기록을
가지고 센 짐작이지 확정이 아니다. 옛 위반 문장을 지워 새 판정인 척하지
않는다(Codex NON-BLOCK).
★`judge_still v16:37` 의 「leaked markers/diagrams/text」가 남아 있다 —
다음 판정에서 정상 로고·간판을 계속 그 이유로 실격 처리하면 **판정 팩도
고쳐야 한다**(제작용 주석·오버레이 누출과 실물 표기를 가르는 수리).
게이트를 켜기 전 그 대조를 본다.

## 10.4 아직 안 한 것

- **「유빅」이 한글로 그려지는 것**: 근원은 `shot_staging` 팩
  `9.202605121441/system.md:201` — `camera_direction`(:199)·`lighting_mood`
  (:200)에는 「영어」가 붙었는데 `key_bg_elements` 에만 없다. 그 값이
  `still_recipe.py:1809` 를 거쳐 `KEY BACKGROUND ELEMENTS:` 로 그대로
  나간다. ★고치면 **샷 연출 계획 단계가 다시 돌아** 하류가 전부 새로
  만들어진다 — 표기 정책 수리(스틸 조립만 움직임)와 비용이 다르다.
  새 표기 정책이 「그 물건이 지닌 문자로」를 말하므로 **다음 판 그림을 보고
  판단**하는 편이 싸다. 사용자 결정 대기.
  ★Codex 경고: 서술문을 영어로 쓰는 것과 **화면 속 고유 표기를 영문화**
  하는 것은 별개다. 참조·대본이 못박은 표기를 일괄 영문화하지 않는다.
- **더 나은 후보로 무료 교체**: 판정 기록만으로 「진 후보가 깨끗한」 샷은
  2개이고, 그중 S29sh6 은 글자 지적이라 이제 결함이 아니다 — 남는 것은
  **S4sh1**(불가능한 룸미러 반사) 하나. 사용자가 갤러리에서 짚는 샷은
  **다시 그리지 않고** 후보만 바꾼다.
- **게이트 구현** — §8 의 사용자 결정 셋이 닫힌 뒤.

---

# 11. 감사 — 「내가 지적한 이전 항목들」 (2026-09-20, 사용자 지시)

글자 건이 **사용자가 푼 것을 뒤 수리가 되잠근 부류**였으므로, 같은 부류가
더 있는지 훑었다. 방법은 **끝점**이다 — 팩 문안이 아니라 **실제로 나간
프롬프트**(`records` 의 `prompt` 칸 239건)를 절 단위로 갈라 셌다.

## 11.1 ★가장 큰 것 — 로봇에게 「모든 캐릭터는 인간이다」가 나간다

| | |
|---|---:|
| `EVERY CHARACTER IS A HUMAN BEING` 이 나간 샷 | **222 / 239** |
| **몸이 곧 신원인 인물**(로봇 등)이 실린 샷 | **84** |
| 그중 이 절이 나간 샷 | **83** |
| 찰리가 실린 샷 | 77 — **77 전부** |

절 본문은 「이야기가 비인간을 명시하지 않는 한 모든 인물은 실제 인간이다 …
사람을 형체·실루엣·덩어리로 줄이지 마라」이고, 바로 뒤에
`EXPRESSIONS ARE ACTED, NEVER ANATOMICAL`(눈꺼풀·얼굴 근육으로 감정을
연기하라)이 **같은 조건으로** 따라 붙는다(`still_recipe.py:2446-2448`).

**★그런데 코드는 이미 누가 비인간인지 알고 있다.**

`app/core/body_identity.py` — 「로봇·사이보그·기계 몸·외계 생명처럼 얼굴만
봐서는 같은 인물인지 알 수 없는 인물」을 `outlook_phase1` 이 **구조 칸**
(`body_identity_chars`)으로 답한다. 글자 판단이 아니다.

이번 판의 값(CP 실측): **C06 찰리 · C07 찰리(홀로그램) · C30 B-200 ·
C44**.

소비처를 전수로 보면 — `reference_phase3_service.py:60,144` ·
`still_recipe_service.py:1320-1323, 3477, 5247` — **전부 참조 고르기**다.
**프롬프트 문안으로는 한 줄도 안 간다.**

그래서 이 결함이 난다(전부 기록에 있는 것들):
- 「가면 안에 **사람 눈**이 그려졌다」(S84sh1) — 「눈을 뜬다」 + 「사람 눈은
  정상 해부」가 로봇에 걸렸다
- 변환이 **로봇 얼굴을 사람 턱으로** 바꿨다(S85sh11)
- 「로봇만 보이면 사람 없음」의 반대편 — 94f763b7 은 **사람 샷으로 만들어**
  고쳤고, 그 결과 인간 규칙도 같이 붙었다

**수리는 싸다 — 값이 이미 있다.** `human_form`·`expression_realism` 을
붙이는 게이트(`if not bg_only or char_names:`)가 **그 샷의 인물이 전부
몸=신원이면 빼고, 섞여 있으면 그 인물을 명시**하면 된다. 새 유료 호출 0.
★다만 문안이 바뀌므로 그 샷들은 다시 그려진다(83샷).

## 11.2 같은 이름의 의상 정본이 갈려 있다

| 이름 | 개수 | short_id |
|---|---:|---|
| 제주도연구원복 | **4** | O32 · O35 · O36 · O37 |
| 민병대제복 | **2** | O15 · O16 |

설명문까지 같다. **아웃룩 3단계의 Phase3(의상 정리·병합)가 안 합쳤다.**
이것이 「이름 없는 무리가 단체 의상을 못 받는다」와 얽힌다 — 의상이 넷으로
갈려 있으면 누구에게 무엇이 배정될지도 흔들린다.

## 11.3 O07 은 **글은 맞고 그림이 틀렸다**

정본 O07 「알록달록우비세트」: **커다란 밀짚모자와 거대한 장화**, 알록달록한
우비 + 「옷이 바깥, 기계가 안이다 — 천이 장갑판을 덮고 장갑판이 옷 위로
올라오지 않는다」(손으로 더한 층 순서).

→ 글에는 밀짚모자·장화가 **있다**. 빠진 것은 **합성 참조 그림**이다
(대표 `033e8b6e`). 정본을 고칠 일이 아니라 그 참조를 다시 뽑는 일이다.

## 11.4 그래서 팩에 더할 칸이 모인다

사용자가 「하드 프롬프팅 아니냐」고 지적한 손수정들의 답이 한자리에 모인다 —
전부 **추출 팩에 칸을 더하는 일**이고, 한 판에 묶을 수 있다:

| 손으로 고친 것 | 팩이 물어야 할 것 | 근거 |
|---|---|---|
| 찰리 크기 | **키·체격** | 실측으로 추출 가능 확인(47명 중 4명만 적고 43명 비움, 지어내기 0) |
| 로봇에 인간 규칙 | — (이미 `body_identity_chars` 가 있다) | **문안 쪽 배선만 하면 된다** |
| 옷 층 순서 | **겉/안 순서** | 합성 팩에 층 순서 문장 0 |
| 총 서술 | 소품 **분리 기준** | 소품 팩 합치기 예시가 하필 「총1, 총2 → 총」 |
| 의상 중복 | Phase3 **병합 기준** | 같은 이름·같은 설명이 2~4개로 갈림 |


## 11.5 ★샷 수 정정 — 84가 아니라 **69**다 (Codex 감사)

첫 보고는 84라고 적었는데, 그것은 **프롬프트 전문 어디든 이름이 나오는
샷 수**다. 새 코드는 `ve_ids` 루프를 돌므로 **PEOPLE 에 드는 인물**만
`nonhuman_names` 에 넣는다.

| 세는 법 | 샷 |
|---|---:|
| 프롬프트 전문 어디든 이름 언급 | 84 |
| **PEOPLE 행에 언급**(코드가 잡는 것) | **69** |
| 캐릭터 참조 라벨에 실첨부 | 69 |
| 전문에만 있고 PEOPLE·참조엔 없는 것 | 14 |

반례 S16sh6: SHOT TEXT 가 「찰리 쪽을 주시하는 **현우의 얼굴**」이라
찰리라는 글자는 있지만 PEOPLE 은 현우뿐이다 — 새 루프는 찰리를 안 넣는다.
전문에만 있는 14: S16sh6 · S21sh5 · S22sh9 · S27sh21 · S51sh14 · S55sh3 ·
S58sh25 · S60sh52 · S62sh15 · S67sh40 · S68sh3 · S71sh17 · S72sh38 · S78sh13.

★69 도 **구매 확정값이 아니다.** 최종 수는 현재 selected 태그별
`(ve_ids + staging 보충) ∩ body_identity_ids` — 즉 **실제 char_names 루프와
같은 집합** — 으로 세고, 그 뒤에 base prompt 와 `roll_prompts[각 후보]`
변화·태그 중복 제거·prev 연쇄를 따로 갈라야 한다.

## 11.6 이 수리가 **안 하는 것** (Codex 정정)

- `cine` 변환이 로봇 턱·홀로그램 재질을 바꾸는 것은 **별도 단계**다.
  이 커밋으로 잡히지 않는다.
- 「모든 비인간」이 아니라 **`body_identity` 로 지정된 인물**의 인간
  기본값 충돌만 푼다. 같은 화의 셰퍼드(개)는 그 명단 밖이다.
- `immobile_physics`·`naturalism`·`support_stillness` 는 이 게이트 밖이고
  그대로다. 예외를 「모든 연기·물리 지시 무시」로 넓히지 않는다.
- 화면 속 **미등록 군중**은 `char_names` 에 안 들어가므로 이 범위 밖이다
  (이름 없는 민병대가 그 자리다).

---

# 12. 기존 화 적용 범위 — 계측 (2026-09-20, 유료 0)

## 12.1 ★새 팩은 **하나도** 자동으로 안 내려간다

| 팩 | 현행으로 잡히나 | 그 단계 지문에 접히나 |
|---|---|---|
| `entity_all/prop` v10 (총 합치기) | ✅ `10.202609202010` | ❌ |
| `entity_extractor_v2` v19 (크기) | ✅ `19.202609202120` | ❌ |

근거:

- `_EntityStepMixin._config_hash` 는 `project_config` + 조건부
  `grounding_binding` 팩 + `episode_carry` 만 접는다 — `entity_all` 은
  **안 접는다**(Codex).
- `EntityDetailStep._config_hash` 는 팩 bytes 를 접지만
  **`uses_chunk_producer(mode)` 가 참일 때만**이다. 이 프로젝트의
  `grounding_mode` 는 그 갈래가 아니라서 `super()` 로 빠진다.
  실측이 그것을 보여 준다:

```
entity_all_prop  status=completed  config_hash=99914b932bd3
entity_detail    status=completed  config_hash=99914b932bd3
entity_t2i       status=completed  config_hash=99914b932bd3
entity_merge     status=completed  config_hash=99914b932bd3
      ↑ 전부 compute_config_hash(None) 의 값 = 팩이 안 접힌 기본값
```

→ **걷기만으로는 소품 목록도 인물 크기도 안 바뀐다.** 기존 화에
적용하려면 그 단계를 **명시 force** 로 돌려야 한다.

## 12.2 force 하면 어디까지 번지나

`step_manifest.get_all_downstream_recursive` 의 `depends_on` BFS 가 정본
이다(Codex). `entity_all_prop` → `entity_extract_prop` → `entity_merge` →
`filter/detail` → `entity_t2i` → 참조·샷 하류.

★**단계 수 · 실재 CP 수 · 재사용 가능한 자산 수 · 예상 유료 호출 수**를
따로 세야 한다. 「팩이 바뀌었으니 하류가 다 돈다」로 보고하지 않는다.

## 12.3 공유 canon 에는 크기가 안 채워진다

`entity_sync_service:313-326` 의 보존 규칙 때문에, 이미 다른 traits 가
있는 **공유 canon** 에는 새 크기가 안 들어간다. 이번 화의
`visual_traits` 는 `episode_notes` 에 남는다(Codex).

## 12.4 그래서 순서

1. 지금 고친 팩은 **다음 화**에서 온전히 산다.
2. 이번 화에 적용하려면 **엔티티 단계 force** 가 필요하고, 그것은
   분석 재실행(유료)이다 — 사용자 승인 사안.
3. 샷 재생성만 하는 길도 있다 — 그러면 **표기 정책·비인간 절·카메라
   문안**(스틸 조립 팩)은 닿지만 **소품 합치기·크기**(추출 팩)는 안 닿는다.

---

# 13. ★§12 앞의 내 진단을 정정한다 (Codex, 슬롯 실측)

나는 「**점수가 자기 판정문을 안 따른다**」고 적었다. **틀렸다.**
`verdicts` 는 `multiroll_gemini.py:683-702` 가 **첫 슬롯 것을 중심으로**
남기는 값인데, `score` 는 `:619-623` 의 **두 슬롯 합산×1000** 이다.
**나는 그 둘을 같은 심판의 말과 점수로 짝지었다.**

슬롯별 `normalized` 를 직접 읽으니 이렇다:

| 샷 | Gemini | GPT | 합산 | 갈래 |
|---|---|---|---|---|
| S2sh3 | **B**(B7·A3) | A(A8·B3) | A | **bgfirst** |
| S36sh7 | 실패(winner=None) | A(A5·B3) | A | **bgfirst** |
| S40sh6 | A(A4·B3) | A(A3·B2) | A | 표준 |

- **S2sh3 는 자기모순이 아니다.** 「다른 로봇이 등장」이라 적은 Gemini 는
  **실제로 B 를 골랐다.** 두 모델이 갈렸고 합산이 A 를 냈다.
- **S36sh7 은 이진 페널티를 아예 안 탄다** — Gemini 가 실패해 GPT 단독
  (`single_reverse`)이고 `:584-593` 에서 바로 반환한다.
- **S40sh6 은 둘 다 A** — 둘 다 문제라고 적고 least-bad 를 골랐다.

## 13.1 그래서 뿌리는 `0.25` 가 아니다

뿌리는 **「상대 순위」와 「최종 사용 가능」이 분리되지 않은 것**이다.
판정은 「둘 중 나은 것」을 성실히 냈고, **둘 다 못 쓸 때 멈추는 자리가
없다.** 가중치를 만지는 것은 그 다음 문제다.

## 13.2 범위도 정정 — 표준 게이트가 잡는 것은 **하나**다

S2sh3·S36sh7 은 `records.bgfirst` 가 있다. 표준 전용 게이트의 대상은
**S40sh6 하나**이고 나머지 둘은 bgfirst 확장 전까지 **비대상**이다.
「셋을 게이트가 잡는다」는 앞 보고를 철회한다.

## 13.3 구조로 잰 것 (Codex, 464 슬롯 전수 · 유료 0)

| | |
|---|---:|
| winner 가 최고점이 아닌 슬롯 | **0** |
| `ranking[0]` 불일치 | **0** |
| 후보 `readings` 행 누락 | **0** |
| **승자 hard 있음 + 상대 hard 없음** | **1** (S4sh1 / Gemini) |

→ 산문 해석 없이 잡히는 우선순위 반례는 **한 건**이다.
★`entities`·`direction`·`verdict` **산문**이 점수와 어긋나는지는 지금
스키마로 코드가 알 수 없다(문자열 칸). 그것까지 보려면 다음 판정 계약에
축별 `match/mismatch/not_visible/uncertain` 같은 **구조 칸**을 받아야
한다 — 이번 게이트에 산문 분석기나 새 유료 심판을 얹지 않는다.

## 13.4 그래서 게이트의 계약 (구현 기본값)

```
① 허용 후보 집합을 먼저 만든다   (hard 가 빈 후보)
② 그 안에서 기존 점수로 고른다   ← 허용 후보가 있으면 **무료 재선택**
③ 하나도 없으면 상한 내 재롤 1회
④ 그래도 없으면 unresolved       ← 정상 확정으로 내보내지 않는다
```

- **범위**: 표준 갈래. bgfirst·confined·conti A/B 는
  `not_applicable_to_this_gate` 로 기록하고 **「검사 통과」로 표시하지 않는다**.
- **재롤**: canonical 샷당 누적 1회. 게이트 재판정기는 **critique master
  와 독립**으로 연결한다(`:1783-1804` 가 아직 master 에 묶여 있다).
- **unresolved**: 그 샷의 정상 확정·후속 cine 구매를 **보류**한다. 후보와
  이유는 남기고 **다른 정상 샷은 계속 간다**. 「least-bad 정상 공개」를
  동의 없이 기본으로 택하지 않는다.
- **오류와 품질을 가른다**: 전송 오류·취소·예산 정지·판정 누락은 품질
  `unresolved` 와 구별하고, 재개가 이미 산 재롤을 또 사지 않게 한다.
- **저장소 기본은 OFF**. 기존 화 유료 적용 때 범위·상한·예상 호출을 따로
  계측해 승인 범위에 넣는다.

## 13.5 종류별 페널티는 **이번에 안 한다**

이진 처리에는 이유가 있다(`:664-678` — 말수·심판 수를 결함 무게로 세던
결함을 닫은 것). `kind` 는 **종류이지 심각도가 아니고**, 임의 가중치가
「사용 불가」를 보장하지도 않는다. 모든 hard 를 막기로 하면 **배열만으로
충분**하고, 특정 종류만 막기로 할 때 `kind` 를 결합·역순·기록까지 함께
넣는다.

★「글자 절 수리만으로 뒤집힌다」는 **가설이다.** 점수를 고정하고 S2sh3
에서 B 의 글자 지적만 뺀 반사실 계산은 B=1.375 > A=1.179 지만, **새 생성·
새 판정의 실측이 아니다.** 옛 기록을 지우거나 새 기준으로 덮지 않는다.

## 13.6 사용자 선택의 지위

다섯 선택은 **선호 증거**이지 「고른 후보는 무결함」 승인도, 오검출률
표본도 아니다. **S13sh10 은 사용자가 A 를 고른 핵심 이유를 모른다** —
기록에는 A 의 다른 결함도 남아 있다. 모른다고 적어 두고 그림과 이유를
따로 확인한다. **점수 보정용 정답으로 학습시키지 않는다.**

---

# 14. 게이트 1단계 구현 — 그리고 ★무료로는 아무것도 안 고쳐진다

## 14.1 넣은 것

`apply_winner_violation_gate()` — 선정 직후, `_sel` 물질화 **전**.

```
① 허용 후보 집합 (실격이 빈 후보)
② 그 안에서 **기존 순위**로 고른다   ← 무료 재선택
③ 하나도 없으면 unresolved
```

- 종착 넷: `clean` · `reselected` · `unresolved` · `not_applicable`.
  **언제나 기록된다** — 「적어 놓고 아무도 안 읽는」 칸을 또 만들지
  않으려면 먼저 빠짐없이 적혀 있어야 한다.
- **미검사는 허용이 아니다** — `readings` 행이 없으면 「실격 없음」이
  아니라 「아무도 안 봤다」다.
- **비대상 갈래는 `not_applicable`** 로 남긴다 — 「검사 통과」로 표시하지
  않는다.
- 정책을 **지문에 접는다**(최종 산출을 바꾸므로).
- 레버 `still_winner_gate_enabled` **기본 OFF** · 표준 갈래에만 전달.

시험 14건. 실제 선정 흐름에서 게이트가 **기록·물질화보다 앞**인지,
공용 함수 기본값이 False 인지, 켜는 곳이 **한 곳뿐**인지도 잠갔다.

## 14.2 ★이 화 기록으로 돌려 본 결과 — 재선택 **0**

표준 갈래 137샷에 걸면:

| 종착 | 샷 |
|---|---:|
| `clean` | **94** |
| `unresolved` | **43** |
| `reselected` | **0** |

**무료 재선택으로 고쳐지는 샷이 하나도 없다.** 표준 갈래의 43샷이
**전부 「둘 다 위반」**이기 때문이다(§6.1 표와 일치 — 표준은 둘 다 위반
43 · 이긴 쪽만 0).

→ **1단계의 값어치는 「고치는 것」이 아니라 「표시하는 것」**이다. 지금은
43샷이 **아무 표시 없이** 정상처럼 나간다. 게이트를 켜면 그것이
`unresolved` 로 남는다.
→ **실제로 고치려면 재롤(2단계)이 있어야 한다.** 그것이 유료 구간이다.

★단, 글자 절 수리 뒤에는 이 43 중 일부가 `clean` 이 될 수 있다(글자
지적이 아예 안 생기므로). **가설이고 새 판정으로 확인할 값이다.**

## 14.3 ★Codex 리뷰 정정 — 「막는다」가 아직 아니다

- **종착이 「언제나」 기록되는 것은 이 helper 를 탄 경우**다. `_sel` 재사용
  갈래는 **더 앞에서 반환**하므로 OFF 로 완주한 옛 record 에는 안 붙는다.
- **행이 있다고 「확인했다」가 아니다** — 칸이 없거나 `null`·문자열이면
  **미검사**다. `(… or [])` 로 받으면 그것이 조용히 「실격 없음」이 된다.
  같은 라벨의 다른 행이 미검사면 **빈 배열이 그것을 지우지 않는다**.
- **허용 후보가 없는 이유가 미검사뿐이면 `incomplete`** 다 — 「판정을 못
  읽었다」를 「모두 실격」으로 바꾸지 않는다.
- ★**확정을 막는 것만으로는 늦다.** `still_recipe_service:3095` 가
  `{prev_tag}_sel.png` 가 있으면 바로 참조로 붙이므로, **실격 그림이
  후속 생성의 권위**가 된다. `gate_blocks_prev_anchor()` 로 끊었다.
- ★**바깥 스텝 지문**을 안 접으면 레버만 켜도 그 스텝이 같은 해시로
  SKIP 해서 샷별 fingerprint 까지 내려가지 않는다 — 켜진 판에서만 접는다.

## 14.4 지금 켜면 실제로 일어나는 일

    · 표준 137샷의 종착이 기록된다 (clean 94 · unresolved 43)
    · 미해결 샷의 그림이 **다음 샷 앵커로 안 쓰인다**
    · **아직 안 되는 것** — cine 구매·자산 승격·완료 표시 차단

즉 「43샷을 막는다」가 아니라 **「43샷을 unresolved 로 분류하고 prev
전파만 끊는다」**까지가 확인된 범위다.

## 14.5 다음

1. 재롤(2단계) 배선 — 게이트 재판정기를 **critique master 와 독립**으로
   연결해야 한다(`:1783-1804` 가 아직 묶여 있다).
2. `unresolved` 의 **실제 소비** — 그 샷의 정상 확정·후속 cine 구매 보류.
3. 오류(전송·취소·예산)와 품질 `unresolved` 를 가르기.

---

# 15. 소비자 문·의존 대기·계수 — 2026-09-20 Codex 리뷰 다섯 바퀴

14.4 에 「아직 안 되는 것」으로 적었던 셋(cine 구매·자산 승격·완료 표시
차단)을 닫았다. 커밋 `231b8a83` → `fce0a1ce`.

## 15.1 문이 **넷**이다

붙잡힌 샷을 막아야 하는 자리는 하나가 아니었다.

    ① cine 구매 · 자산 승격 · 완료 표시   (새 선정 + 재사용 둘)
    ② 다음 샷의 **앵커**                   (파일 · DB 대표 둘 다)
    ③ 스텝 **verify**                      (옛 대표가 있어도 완료 아님)
    ④ 스텝 **실행 계수**                   (completed / failed)

이 넷이 처음에는 **서로 다른 종착을 읽고 있었다**. cine 문은
`unresolved` 만, prev·verify 는 `incomplete` 도 봤다 — 같은 샷이
**변환은 사고 앵커로는 못 쓰이는** 어긋남이 났다. `GATE_HOLDING_OUTCOMES`
하나로 모았고, 적용 경계(켜짐/꺼짐)도 `gate_is_on()` 한 자리로 모았다.

★**공통 상수만으로는 공통 판정이 아니다**(Codex). 상수를 import 해 놓고
소비자가 각자 「꺼져 있으면」을 다시 적으면 한쪽만 고쳐진다.

## 15.2 의존 대기 — 붙이는 것보다 **푸는 것**이 어려웠다

A→B→C 에서 A 가 붙잡히면 B 를 건너뛰는 것만으로는 부족하다. B 의 기록에
아무것도 안 남으면 **C 가 B 의 옛 `clean` 을 읽고** 옛 그림을 앵커로
쓴다 — 한 샷 뒤에서 다시 뚫린다. 그래서 B 에 `blocked_dependency` 를
**남긴다**.

그런데 남기기만 하고 **푸는 길이 없었다**. A 가 나중에 clean 이 되어도
B 의 입력 지문이 그대로라 `multiroll_select` 가 기존 record 를 그대로
돌려주고, 기록의 `gate` 는 여전히 대기라 문이 **영원히** 막는다.
「부모가 풀리면 다시 평가」는 주석뿐이었다.

    release_dependency_hold(records, tag)
      · 대기 **한 겹만** 벗긴다
      · `prior` 의 원래 판정으로 되돌린다 — unresolved 였으면 unresolved
      · `prior` 가 없었으면 판정 칸을 **지운다**(없던 판정을 안 만든다)
      · 새 그림을 사서 푸는 것이 아니다 — 메타만 바뀐다
      · 저장이 실패하면 **메모리도 되돌린다**

그리고 이 대기는 **지출 흔적이 아니다** — `_jit_tag_snapshot` 이 대기
겹을 벗기고 견준다. 안 그러면 메타만 바뀐 방문이 「지출」로 읽혀 JIT
재생성 계수가 거짓으로 오른다.

★**게이트 ON 재개는 JIT 검증 ON 을 요구한다.** JIT 가 꺼지면 완료 샷이
몸통에 안 들어와 **해제 자리에 못 온다** — 「ON 으로 재개하면 저절로
복구된다」가 성립하지 않는다.

## 15.3 세는 자리 — 「못 읽었다」도 「안 봤다」도 합격이 아니다

`verify` 와 실행 계수가 **같은 집합 계산**을 쓴다.

    V = selected 안에서 **파일이 있는** 대표의 고유 still_id
    H = 같은 정책에서 보류된 still_id (직접 보류 + 의존 보류)
    완료 = V − H

`대표 수 − len(H)` 로 빼면 **원래 대표가 없던** 보류 샷을 두 번 뺀다.

붙잡는 자리는 셋이다.

    · 종착이 붙잡는 것         → 보류
    · 기록을 **못 읽었다**     → 검사 불가 (완료 0)
    · 이 샷의 **판정이 없다**  → 미판정 → 보류

★「명시 `not_applicable`」과 「판정 없음」은 **다르다**. gate 키가 없다는
것만으로 「bgfirst 라 비대상이겠지」라고 알 수 없다.

★**자산을 지우거나 대표를 내려서 실패를 표현하지 않는다.** 세는 자리에서만
뺀다. `primary_count`(실제 대표 수)와 정책상 완료 수를 따로 남긴다.

## 15.4 ★내 시험이 결함을 정답으로 잠근 것 — 이 판에서만 다섯 번

1. 소스 문자열만 본 시험 — 연쇄를 못 봤다
2. 뒤집어도 통과하는 DB 대역
3. CP 행의 **칸 수**를 세고 쓴 횟수라고 했다
4. 「`incomplete` 면 저장 1」
5. 「기록이 없으면 빈 집합」을 `does_not_invent_failures` 라는 **이름으로**
   못박았다 — 그 이름이 맞는 자리는 **꺼진 판**뿐이다

그리고 A→B→C 시험의 첫 판은 **해제 코드를 지워도 통과했다.** 대역이
프로덕션과 두 군데 달랐다.

    ① 선정 뒤 record 를 저장하지 않았다 (`run_branch_select._persist`)
    ② 지문이 맞아 **다시 안 그리는** 재사용 갈래를 흉내 안 냈다
       → 재생성이 대기를 대신 지워 줬다

둘을 맞추니 Codex 가 적은 그대로 「S1sh2 영구 보류」가 재현됐다.
**고치기 전 판에서 실패하는지**를 매번 확인하고서야 시험으로 인정했다.

## 15.5 ★ON 의 값은 「라벨링은 공짜, 지문 접기가 돈」

이 화(239샷) 기록을 그대로 읽어 셌다. 유료 호출 0.

    판정 기록      239샷 전부 `gate` 키 **없음** (게이트가 안 돌았다)
    갈래           confined 8 · bgfirst 93 · conti A/B 1 · **표준 137**
    이미 있는 재료 239샷 전부 `readings` 있음
                   → 이긴 후보에 `hard_violations` 달린 샷 **81**
                     (**전 갈래** 합이다 — 표준 43 · bgfirst 35 · confined 3)
                   → 현행 적용 범위(표준)의 예상 `unresolved` 는 **43**

그런데 `winner_gate` 가 **입력 지문에 접힌다**. 저장된 지문은 안 접고
만든 것이라, ON 으로 재개하면 **표준 137샷이 전부 지문 불일치** →
`_clear_outputs` → 롤부터 다시 산다. JIT 재생성 상한 64 에서 래치가
먼저 걸린다.

즉 **판정에 필요한 것은 이미 디스크에 있는데**, 지문 접기 때문에 137샷을
다시 사게 된다. ## 15.6 ★Codex 정정 — 내가 두 가지를 틀리게 적었다

**① 「최대 81샷이 보류」는 틀렸다.** 81 은 **전 갈래**에서 「옛 승자에
위반이 있다」는 수다. 현행 적용 범위는 표준 137 이고 그 안의 예상
`unresolved` 는 **43**. 여기에 의존 보류·근거 누락·미판정이 더해지므로
최종 partial 은 그 합집합의 고유 tag 수 — 43 도 81 도 상한이 아니다.

**② 「137샷을 다시 산다」도 실측이 아니다.** 137 은 **그 지문 항목이
바뀌는 모집단**이지 이미지 구매 137회가 아니다. 의존 보류로 안 닿는 샷도
있고, 닿으면 롤 수·판정 슬롯·cine 가 따로 든다. 그리고 이번 판에는
표기·인간 규칙 같은 **진짜 프롬프트 변경**도 있다 — `winner_gate` 를
지문에서 뺀다고 그것들의 정당한 stale 까지 사라지지 않는다.

★옛 `readings` 로 **무료 감사**는 되지만, 그것은 **옛 글자 금지 계약**
아래 내려진 판정이다. 새 허용 계약으로 다시 판정한 것처럼 보고하면 안
된다.

## 15.7 ★ON 비용 제동이 한 번도 안 센다 — 고쳤다

게이트 미해결 문은 `still_recipe_service.py:5601` 이고 JIT 상한 계수는
`:5880/:5896` 이다. **문이 앞이다.** 그래서 완료 샷이 지문 불일치로
롤부터 다시 사고 문에서 붙잡히면 `jit_regen_count` 가 **한 번도 안
오른다** — 다음 샷의 `:3095` 가드는 안 오른 수를 본다. 상한 64 가
영영 안 걸리고 대상 전부를 그대로 다시 사게 된다.

    · 문에서 **지출이 났으면 세고** 붙잡는다 (두 자리 다)
    · 그런데 **게이트 종착은 지출 흔적이 아니다** — `_jit_tag_snapshot`
      에서 `gate` 를 통째로 뺐다. 이름표를 붙이거나 바꾸는 데는 돈이
      안 든다. 남겨 두면 두 방향으로 다 틀린다:
        · 의존 대기가 붙었다 풀리는 것만으로 「지출」
        · 옛 기록에 판정만 채우는 **무료 소급**이 「지출」
      정말 돈이 나간 방문은 롤·판정문·수리 기록·
      `shot_run_spend_attempt_count` 가 같이 움직여 그대로 잡힌다.

시험: 상한 1 + 독립 완료 샷 둘 — 첫 샷이 돈 쓰고 붙잡히면 **둘째는 유료
경계에 못 온다**(생성 호출 1). 대조로 **판정 이름표만 채운 방문은 지출
0**(래치 없음).

## 15.8 다음 — 소급 적용은 ③으로 (Codex 설계 답)

②(지문에서 빼고 소급 판정)의 **목적**은 맞지만, 방법은 셋을 갈라야
한다.

    · **생성 지문**(후보를 다시 살 것인가)에서 `winner_gate` 를 뺀다
    · **정책 신원**은 `gate` 안에 남긴다 — 정책 버전 + 적용 범위 +
      평가에 쓴 근거의 식별자(기존 지문·labels·readings digest).
      `clean` 이어도 「어느 정책으로 허용했나」는 바뀐다
    · **선택 라벨·선정 파일 sha 가 바뀌면** 그때 최종 산출과 그걸 쓰는
      cine·prev 를 갱신 대상으로 삼는다

★내가 「재선택될 때만 정책 지문이 움직인다」고 적은 것은 **틀렸다**.
 같은 A 라도 「허용된 A」와 「unresolved 인 A」는 정책 상태가 다르다.
 정책은 **항상** 식별하고, 옛 롤 파일의 수명을 거기 묶지 않는 것이다.

소유 자리는 `multiroll_select` 의 **무호출 반환 앞**(`:1712-1715` 뿐
아니라 `:1716-1725` 도) 공통 함수 한 자리. 새 판정 직후(`:1957`)와
**같은** `apply_winner_violation_gate` 를 쓴다. 생성 지문을 먼저 분리하지
않으면 `:1699` 가 이미 후보를 지운 뒤라 늦다.

    · 실제 selected·ranking·labels·readings 를 그대로 쓴다.
      수정본이 최종이면 **초기 롤 readings 를 그 수정본의 판정으로
      오인하지 않는다**(이 화 기록이 전부 `critique_skipped` 라는 것이
      이번 범위를 좁히는 근거)
    · `reselected` 면 **기록만 B 이고 파일은 A** 인 채 돌려주지 않는다 —
      기존 후보에서 물질화하고 persist 한다
    · 근거가 없으면 **미검증으로 남긴다** — 무료 소급을 명분으로 생성이나
      새 VLM 호출을 하지 않는다
    · gate 만 갱신한 방문은 **생성 저자가 아니다**

★비표준 102샷(bgfirst 93 + confined 8 + conti A/B 1)에 `not_applicable`
 이 안 찍히면 verify 가 계속 `unjudged` 로 붙잡는다 — 소급 경로가
 비대상도 **명시**해야 한다. 그리고 `gate_holds`·prev 는 아직 「판정
 없음」자체를 안 막는다(verify 만 센다) — 소급에서 대상·비대상·미판정을
 prev·cine 보다 **먼저** 확정해야 한다.

## 15.9 무료 소급 — 캐시 반환에 정책을 적용한다

정책을 생성 지문에서 떼고 나면 새 구멍이 난다. **지문이 맞는 기록은
판정을 안 거치고 캐시로 그대로 나간다** — 정책을 켜도 옛 기록에는 영영
판정이 안 붙고, verify 는 그 샷들을 계속 미판정으로 붙잡는다.

`reapply_gate_to_cached_record` 가 **호출 0** 으로 이미 산 후보·이미 받은
판정문에만 정책을 적용한다. 새 판정 직후와 **같은**
`apply_winner_violation_gate` 를 쓴다 — 판정기를 복제하지 않는다.

    · 무호출 반환이 **둘**이라 두 곳 다 지난다
    · **재선택이면 선정 파일까지** 그 후보로 맞춘다 — 기록만 B 이고
      파일은 A 인 채 나가면 재개에도 그 어긋남이 남는다
    · **수정본이 최종인 기록은 손대지 않는다** — 저장된 판정문은 초기
      롤을 본 것이라, 그걸로 수정본을 판정하면 **다른 그림의 판정**을
      이 그림에 붙이게 된다 (`incomplete` 로 남긴다)
    · 근거나 선택이 없으면 미검증 — 소급을 명분으로 생성도 새 판정
      호출도 하지 않는다
    · **비대상도 `not_applicable` 로 적는다** — 없으면 비표준 102샷이
      계속 미판정으로 붙잡힌다
    · **의존 대기는 걷기가 소유한다** — 여기서 풀지 않는다
    · ★**두 번째 방문은 아무것도 다시 안 쓴다**(정책·근거가 같으면
      건너뛴다). 캐시 방문마다 메타를 새로 적으면 「지출 흔적」이 매번
      움직여 재생성 계수가 거짓으로 오른다

## 15.10 판정이 없는 앞 샷은 앵커가 아니다

켜진 판에서는 **모든 갈래가** 판정을 적는다(비대상도 `not_applicable`).
그러니 칸이 비어 있다는 것은 「비대상이라 안 적었다」가 아니라 **이 샷을
아직 안 봤다**는 뜻이다.

실제로 그런 자리가 있다 — `record` 가 없는 완료 샷은 지문을 잴 근거가
없어 몸통에 안 들어오고 그대로 건너뛴다. 그 그림이 뒤 샷의 권위가 되면
아무도 안 본 것이 연쇄로 번진다. verify 는 그것을 미판정으로 붙잡는데
앵커 쪽만 통과시키면 **두 소비자가 어긋난다**.

★새 그림을 사서 푸는 것이 아니다. 그 앞 샷의 판정을 되살리면(기록 복구 ·
 다시 걷기) 뒤 샷은 그대로 살아난다. 꺼진 판은 종전 그대로다.

## 15.11 아직 안 한 것

    · 재롤(2단계) 배선 — 게이트 재판정기를 critique master 와 **독립**으로
    · `unresolved` 가 남은 샷의 실제 복구 — 지금은 **보류로 멈추는** 것까지
    · 기존 화에 켜는 것 — 「정책만 바뀐 경우」와 「진짜 생성 입력도 바뀐
      경우」를 **따로 계측**한 뒤에 범위를 정한다. 이번 판에는 표기·인간
      규칙 같은 실제 프롬프트 변경도 있어 그 stale 은 정당하다

## 15.12 Codex 리뷰 여섯 바퀴째 — 재선택 결손 경로

**BLOCK: 후보 파일이 없을 때 원래 선택을 못 되돌렸다.**

소급이 A→B 를 정하며 `record["selected"] = B` 를 **먼저** 했다. 그 뒤
호출부가 B 의 롤 파일이 없는 것을 알고 되돌리려 해도, 원래 값이 이미
지워진 뒤라 `initial_selected` 를 **새로 덮어쓴 gate 에서** 찾게 되고
fallback 도 B 였다. 결과: 기록은 B · `_sel` 은 A.

그 다음 방문이 더 나쁘다 — 미검증에 근거가 없어 다시 평가하는데, 이제
`selected` 가 B 이고 B 의 판정은 허용이라 **`clean` 이 된다**. 라벨이 더
안 바뀌니 파일 확인도 건너뛰고, **실격 A 가 B 의 합격 판정과 생성 계보를
달고 나간다.**

    · 선택은 **물질화가 성공한 뒤에만** 확정한다
    · 파일이 없으면 선택도 `_sel` 도 **그대로** 두고 미검증
    · 그 미검증에는 **`evidence` 를 안 채운다** — 채우면 영원히 건너뛰어
      파일이 복구돼도 살아날 길이 막힌다

시험: 두 방문 모두 A/A/미검증·생성 0 → 파일 복구 → B/B/재선택 → 그 뒤
방문 무재기록.

**NON-BLOCK 도 닫았다**

    · 지출 스냅샷에서 `gate` 를 **자식 기록에서도** 뺀다 — conti A/B 의
      두 자식에도 소급이 `not_applicable` 을 쓰기 때문. 자식의 **진짜
      유료 흔적**(critique 등)은 그대로 잡힌다
    · `fixed_final` 문구를 「검사·수리 이력이 있는 캐시는 **보수적으로**
      제외」로 좁혔다 — 실제 수정본 승리를 보는 것이 아니다

**아직 안 한 것 (Codex NON-BLOCK 2)**

무료 재선택은 `gate` 말고 `selected` 와 산출 bytes 도 바꾼다. 그래서
지금 서비스는 그 방문을 **재생성 방문으로 센다**. 「후보 생성·VLM 0」과
「JIT 재생성 계수 0」은 **아직 같은 말이 아니다.** 이번 화는 재선택 0 이라
발동하지 않는다.

## 15.13 보고 정정 — 「137샷을 다시 샀다」

**사지 않았다.** 137 은 그 지문 칸이 바뀌는 **표준 샷 모집단**이고, 내가
확인한 것은 「켜면 그 모집단이 재생성 대상이 된다」는 **정적 사실**이다.
이 판의 유료 호출은 **0** 이다. 실제로 걸었다면 의존 보류로 안 닿는 샷도
있고, 닿으면 롤 수·판정 슬롯·cine 가 따로 든다.

## 15.14 ON 시점 — Codex 판단

**이 완주 화에 옛 판정을 소급해서 지금 생산 게이트를 켜지 않는다.**
그 판정에는 **이미 폐기한 글자 금지**가 섞여 있어, 43 은 「옛 계약상 직접
보류」이지 최신 사용자 계약으로 확인된 결함 43개가 **아니다**.

    · 지금은 그 결과를 **감사·육안 검토**로 남긴다
    · 최신 생성·판정 계약을 적용하는 **승인된 다음 생성**에서 1단계를 켠다
    · 2단계 완성을 기다릴 필요는 없다 — 다만 **보류가 남으면 공개 완료가
      아니라는 운영 조건**이 필요하다

## 15.15 2단계 설계 — Codex 판단 (시작 전 고정)

**① 「종류 칸 확장」과 「유료 재롤」은 다른 축이다.** 지금 계약이 모든
`hard` 를 막으므로 **문자열 배열의 빈/안 빈 검사로 시작**하면 된다.
일부 종류만 재롤할 때에만 typed kind 가 필요하다. **옛 문장에서 종류를
추측하지 않는다.**

**② 재롤 뒤에도 허용 후보가 없으면 `unresolved` 유지가 기본이다.**
least-bad 를 정상본으로 내보내지 않는다. 전송 실패·취소·예산·판정 누락은
품질 `unresolved` 와 **별도 미완료**다. 이것은 §13.4 에 이미 적혀 있으므로
§7 의 옛 양자택일을 다시 열린 결정처럼 두지 않는다.

**③ 재롤 상한은 같은 입력·정책의 canonical 샷당 누적 1회 + 주행 단위
승인 상한.** `_mark_spend_attempt_once` 는 **방문당 진입표라 중복 구매를
못 막는다.**

    · 재롤 시도 식별자·상태를 **발송 전에** 남긴다
    · 생성 완료 파일·sha·실제 호출 계보를 저장한 **뒤에** 재판정으로 간다
    · 재판정만 실패하면 재개는 **같은 재롤 파일을 다시 판정**한다
    · 발송 결과가 불명이어도 **자동으로 새 1회를 주지 않는다**

**그 밖**

    · 재롤 후보와 초기 후보의 **허용 집합 안에서만** 순위를 비교한다
    · 재판정기는 critique master 와 **독립으로 명시 배선**하되 사용자가
      끈 master 를 켜지 않는다
    · 쓸 재판정 콜백과 `readings` 보존을 **이미지 발송 전에** 검증한다
    · 표준 갈래부터 — bgfirst·confined 확장은 별도
    · **계보·회계**: 새 재롤이 져도 구매는 기록하되 **최종 산출의 저자를
      그 재롤로 바꾸지 않는다.** 재개로 옛 재롤이 채택되면 그 파일의
      원래 생성자를 유지한다
    · ★지금 스냅샷에서 `gate` 를 빼므로, **유료 시도 이력을 `gate` 안에만
      넣으면 또 지출이 사라진다** — 기존 지출 신호·별도 시도 상태와 잇고
      새 후보·새 판정에는 **별도 역할/태그**를 준다
    · 정상 재롤 1회도 cross-model 재판정이면 **이미지 1 + VLM 2** 다 —
      「유료는 이미지뿐」이 아니다

---

# 16. 2단계 — 못 쓸 산출을 **다시 산다**

1단계는 「못 쓴다」고 **분류하고 멈추는** 것까지였다. 실측으로 이 화
43샷은 **무료 재선택이 하나도 안 됐다**(둘 다 못 쓰는 것) — 실제 복구는
여기서만 난다.

레버 `STILL_GATE_REROLL_ENABLED`, **기본 OFF**. 게이트가 꺼져 있으면
아무 뜻이 없다.

## 16.1 한 샷의 유료

    이미지 **1** + 재판정 **1**(cross-model 이면 VLM **2**)

「유료는 이미지뿐」이 아니다.

## 16.2 상한 — 두 축

**① 같은 입력·정책의 canonical 샷당 누적 1회.** 재개까지 합산한다.
`_mark_spend_attempt_once` 는 **방문당 진입표**라 중복 구매를 못 막는다.

    record["gate_reroll"] = {policy, for_fingerprint, attempt_id,
                             state, label, sha256, adopted, …}

`gate` **밖**에 쓴다 — 지출 스냅샷이 `gate` 를 빼므로(§15.7) 시도 이력을
그 안에 넣으면 **지출이 또 사라진다**.

**② 주행 단위 승인 상한** (`STILL_GATE_REROLL_RUN_LIMIT`, 기본 24).
호출부가 소유한다 — 선정 함수는 샷 하나만 안다.

## 16.3 단계를 나눠 적는다

    sent      발송했다(결과 불명 포함)  ← **그림을 보내기 전에** 적는다
    produced  파일을 받았다 + sha
    judged    재판정까지 끝났다

    · 발송만 하고 죽었다 → 다음 방문은 **새 1회를 안 준다**. 사람이
      확인하고 기록을 지워야 다시 산다
    · 재판정만 실패했다 → 재개는 **같은 재롤 파일을 다시 판정**한다
      (새 이미지 0)

## 16.4 사기 전에 확인한다

    · 재판정 콜백이 있는가
    · 그 판정이 **`readings` 를 남기는가** — 게이트는 그 칸으로만 판단한다
    · cross-model(`owns_order`) 갈래인가

★`emits_readings` 는 **자기 스키마를 본다**(`"readings" in required`) —
 선언만 받으면 틀려도 안 막힌다.

돈을 쓰고 나서 「판정할 것이 없다」를 알면 그 돈은 버린 것이다.

## 16.5 종착

| 종착 | 뜻 | 최종 산출 |
|---|---|---|
| `clean` | 이긴 후보에 위반 없음 | 정상 |
| `reselected` | 허용되는 **기존 후보**로 바꿈(무료 또는 재롤 뒤) | 정상 |
| `resolved` | **재롤본**이 허용 후보로 채택됨(유료) | 정상 |
| `unresolved` | 재롤 뒤에도 허용 후보 없음 | **보류** |

★**허용이 점수보다 먼저다.** 재롤 후보와 초기 후보를 **한 집합**으로 놓고,
 허용 집합 **안에서만** 순위로 고른다. 하나도 허용되지 않으면
 `unresolved` 유지 — **least-bad 를 정상본으로 내보내지 않는다.**

★재롤이 **져도 구매는 기록한다**(`sha256` 포함). 산 것을 없던 일로 하지
 않는다. 다만 최종 산출의 저자를 진 재롤로 바꾸지 않는다.

## 16.6 ★재롤이 끝난 기록은 소급이 다시 보지 않는다

그 종착은 **재판정**(초기 후보 + 재롤 후보)이 냈다. 소급(§15.9)이 쓰는
근거는 `record["readings"]` = **초기 판정**이라, 정책이 올라가 다시 보면
**산 그림을 버리고** 옛 판정으로 덮어쓴다. `gate.reroll` 이 있으면 소급을
건너뛴다.

## 16.7 아직 안 한 것

    · bgfirst·confined 확장 — 지금은 **표준 갈래만**(재판정 후보 수 3 이
      이 갈래의 롤 수 2 에 맞춰져 있다)
    · 무료 재선택이 「JIT 재생성 계수 0」은 아직 아니다(§15.12)
    · 2단계 `kind` enum — 모든 hard 를 막는 지금 계약이면 **배열 검사로
      충분**하다. 일부 종류만 재롤할 때 넣는다

---

# 17. 계측 — **기록이 바뀐 것**과 **돈이 나간 것**

JIT 재생성 제동은 「record 묶음이 움직였나」로 지출을 쟀다. 그런데 캐시
소급 재선택은 **생성도 판정도 안 사면서** `selected` 와 산출 bytes 를
바꾼다 — 그것만으로 「재생성 방문」으로 세면 **상한이 무료 행동에
소모되고** 보고도 틀린다.

## 17.1 두 축으로 가른다 (영구 boolean 하나가 아니다)

    ① 이번 방문의 **구조적 행동**
       record["gate_select_action_this_run"]
         = {action, from, to, to_sha, materialized}
       · `_this_run` 접미라 지출 스냅샷이 걸러 낸다
       · **구매 표식이 아니다** — 「무엇을 했나」일 뿐
       · `run_multiroll_select` **진입에서 비운다**(아래 17.3)

    ② **유료 구간 진입**
       shot_run_spend_attempt_count 의 움직임
       · 이것도 「이미지 몇 장·VLM 몇 회」가 **아니다**

**무료로 빼는 것은 둘 다일 때뿐**이다. 아직 못 재는 경로를 0 으로 치고
제동을 넓게 풀지 않는다. 계수를 못 읽으면 **무료가 아님**으로 둔다.

★**`gate.outcome` 으로 무료를 판정하지 않는다** — `reselected` 라도 C 를
 **유료로 사고** 기존 B 가 이긴 경우가 있다.

## 17.2 ★본체 계수 하나로 묶음 전체를 지우지 않는다

「공유 배경이 유료면 본체 계수도 오른다」는 **추측이었지 코드의 보장이
아니었다**. `_run_groupbg` 도 cine 도 provider 를 **직접** 부르고, 본체
계수를 올리는 것은 `multiroll_select` 안의 record 뿐이다.

그래서 `_jit_tag_snapshot(..., selected_override=...)` 로 **선택 한 칸만
옛 값으로 되돌린** 스냅샷이 `jit_snapshot_before` 와 **같아야** 무료로
본다.

    선택만 바뀜                    → 무료
    선택 + 자식 `::cine` 움직임    → 유료로 남김
    선택 + 공유 `groupbg::` 움직임 → 유료로 남김
    선택 + 본체 판정문 바뀜        → 유료로 남김

특히 **변환이 기각·실패로 끝나 최종 bytes 가 같은** 경우가 위험하다 —
그때 지출 흔적이 지워지면 상한도 신규 메타 영속 검토도 함께 건너뛴다.

## 17.3 `_this_run` 은 **이름뿐이었다**

`_scrub` 은 **비교용 복사본**에서 키를 빼는 것이지 기록을 비우지 않는다.
그래서 한 번 재선택한 샷이 **무변경 방문마다** 「이번 무료 재선택」으로
집계되고, 그 방문이 변환만 다시 시도하면 **유료 흔적까지 지운다**.
→ 진입에서 `record.pop`. 방문 uid 를 지출 스냅샷에 **새로 넣지는 않는다**
  (거짓 지출이 된다).

## 17.4 ★보고만 고치고 계수는 안 고쳤던 것

`_records_changed = False` 만 만들면, bytes 가 달라졌을 때 「동일 산출」
두 갈래를 **둘 다 건너뛰고** 무조건 증가에 도착한다 — 같은 방문에
「무료 재선택」과 「입력이 낡아 재생성」을 **둘 다 보고하고 래치까지**
건다.

★내 시험이 **로그만 봤다**. 「로그 한 문장으로 계수 확인을 대신하지
 말라」(Codex). 이제 상한 1 로 두고 **래치 파일**을 본다.

## 17.5 pending 물질화 복구 — **과다 계상을 좁혔다** (2026-09-20)

종전에는 이 갈래를 보수적으로 **유료**로 셌다. 재개가 남기는 기록 변화를
되돌릴 손잡이가 없었기 때문이다. 이제 좁혔다.

재롤의 순서는 「예고(`pending_pick`) → 파일 교체 → 확정」이다. 파일 교체
에서 끊긴 뒤의 **재개 방문이 하는 일은 파일을 옮기고 예고를 확정으로
바꾸는 것뿐**이다 — 이미지도 판정도 **안 산다**. 그런데 기록에서는 두
칸으로만 드러난다:

    ① `gate_reroll.pending_pick` 이 **사라진다**
    ② `critique_skipped` 가 **새로 적힌다** (끊긴 방문이 이 칸을 적기
       **전에** 죽었기 때문이다 — 죽은 자리가 그 앞이다)

그래서 되돌릴 손잡이 둘을 만들었다.

    · `_materialize_reroll_pick(resumed=True)` 일 때만 **무엇을
      이어받았는지**(`gate_select_action_this_run.resumed_pending_pick`)를
      남긴다. 같은 방문에서 **사고 곧바로 확정하는** 길은 안 남긴다.
    · `_jit_tag_snapshot(reroll_pending_override=...)` 가 그 한 칸만
      **되돌려** 견준다.
    · `critique_skipped` 는 스냅샷에서 **뺀다** — 「안 샀다」는 지출이
      아니다.

★**`gate_reroll` 을 통째로 거르지 않는다.** 진짜 재롤(이미지 1 + 재판정
 1)의 유료 흔적이 거기 남는다 — `gate` 안에 넣지 않은 이유가 그것이다.
★`adopted` 는 안 건드린다. 예고를 적을 때 이미 같은 값으로 확정돼 있어
 재개가 바꾸지 않는다. 혹시 달랐다면 묶음이 어긋나 **유료로 세는
 쪽**(안전한 쪽)으로 떨어진다.
★**무료는 방어선 둘의 AND 다** (Codex 정정). 「지출 계수가 정한다」로만
 적으면 반쪽이다. 둘을 **같이** 요구한다:

    ① 유료 구간 **진입 계수가 안 움직였다**
    ② 선택과 예고 **두 칸만 되돌렸을 때 묶음 전체가 같다**

 그래서 재판정만 다시 산 방문은 ①에서, 본체 진입을 안 올리는 cine·공유
 배경의 기록 변경은 ②에서 걸린다.

★그리고 **표식은 무료를 선언하지 않는다.** `resumed` 를 유료 길에도
 억지로 달아 봤더니 시험이 **그대로 통과**했다 —
 `shot_run_spend_attempt_count` 가 이미 올라 ①이 False 이기 때문이다.
 표식은 「무엇을 되돌려 견줄까」만 말한다.

## 17.5.1 ★시험이 잠근 **범위**를 좁혀 적는다 (Codex NON-BLOCK)

넓혀 쓰면 안 되는 것 둘 — 시험 docstring 에도 적었다.

    · cine 시험은 변환이 **성공**으로 응답하고 둘째 입력에서 옛 `::cine`
      기록을 뺀 판이다. 잠근 것은 **「신규 cine 호출이 있는 방문을 무료
      목록에서 제외」**까지다. 「기존 cine 기록을 보존한 재개에서 기각·
      실패 + 최종 bytes 동일이어도 래치·메타가 정확」은 **아니다**.
    · 세 번째 방문 시험의 DB·저장 대역은 **상태를 이어 가지 않는다**.
      「추가 자산 저장 0」의 증명으로 쓰지 않는다 — 잠근 것은 **행동 표식
      부재와 무료 보고 0** 이다.

## 17.6 아직 안 한 것 — 정확한 호출 수

발송 계측의 **원천은 호출 경계**이고, `llm_call_log`·Opik 은 결과·사용량을
붙이는 **감사 경로**다.

    · `generate_image` 입구 1회 **≠** 실제 전송 1회 — 재시도 루프가
      함수 **안**에 있다
    · Router 에도 자체 retries 가 있어 그 진입만 세면 「물리 전송 총수」가
      아니다
    · ★**「로그 행 없음 = 전송 0」이 아니다** — 기록 실패를 삼키는 자리가
      있고, 재시도 `continue` 가 실패 로그보다 **앞**이다
    · 전송 횟수와 **청구 확정 횟수**도 다르다

발송 시도 · 접수 확인 · 성공/실패/결과 불명을 **attempt ID** 로 이어
방문·본체/자식/공유·종류(image/VLM)·물리 모델과 함께 남기는 것이 다음
확장이다. 그때까지 **미계측 경로의 보수적 제동은 그대로 둔다**.

# 18. 갈래 확장 — bgfirst·confined 도 대상 (2026-09-20 ③)

## 18.1 무엇이 막고 있었나

1단계를 켤 때 「원리상 불가능해서가 아니라, 가장 흔한 경로에서 결과를
먼저 보려고」 표준 갈래만 켰다. 그런데 **막고 있던 것이 하나 더** 있었다:
재판정기를 **후보 3개짜리 하나로** 지어 뒀다. 갈래마다 롤 수가 달라서
그대로 넓히면 라벨이 안 맞는다.

## 18.2 고친 것

    · 재판정기를 **(후보 수, 판정 머리말)마다** 짓고 재사용한다.
      후보 수 = **그 갈래의 롤 수 + 1**.
    · 머리말도 **그 갈래 것**이다. bgfirst 는 중립 머리말을 쓴다 —
      체인·무콘티 후보는 참조·문안이 달라 기본 머리말('generated from
      this')이 **거짓**이기 때문이다. 재판정만 기본 머리말로 물으면
      **다른 질문**이 된다.
    · 재판정 신원에 후보 수와 머리말 digest 를 같이 적는다.
      ★**이 두 칸은 지금 「감사 메타」다** (Codex NON-BLOCK 정정).
       `reroll_attempt_matches` 는 **정책과 생성 지문만** 읽는다 — 이 두
       칸이 재개에서 시도를 가르지 **않는다**. 갈래 차이는 이미 **생성
       지문**이 가른다(롤 수·`roll_refs`·판정 머리말이 거기 접혀 있다).
      ★이 지적을 해결한다고 **평가 메타를 생성 지문에 다시 넣지 않는다** —
       넣으면 켜는 것만으로 옛 후보를 다시 산다(§15.5 와 같은 병).
    · 배선은 `_run_branch` **한 자리**. 호출 자리는 「대상인가」(`gate=`)
      와 「제 머리말」만 말한다.

대상: 표준 · confined · bgfirst 체인단독 · bgfirst 2택1 — **넷**.

## 18.3 ★최종 라벨과 **출처 갈래**는 다르다 (Codex BLOCK)

게이트 재롤본 `C` 는 `A` 나 `B` 의 **문안과 참조를 물려받아** 만들어진다.
그런데 채택되면 `selected` 가 `C` 라, **라벨만 보면** `C != A` 여서
bgfirst 2택1 이 그것을 **무콘티 승**으로 적었다 — 실제로 쓴 재투영 배경·
콘티가 빠지고 **안 쓴 플레이트**가 직접 입력으로 붙는다. 로그 오기가
아니라 **최종 자산의 참조 계보 오기**이고, `C` 를 산 뒤의 **정상 성공
경로**에서 난다.

`_reroll_origin_label(record, selected)` 로 **출처를 읽는다** —
최종 라벨이 장부의 재롤 라벨이면 `from_selected`(없으면 게이트의
`initial_selected`)를 쓰고, 초기 후보가 최종이면 그 라벨 그대로다.

★`C` 를 **무조건 체인으로 치면 반대로 틀린다** — `B` 에서 만든 `C` 는
 무콘티다. ★출처는 **기록에서** 읽으므로 캐시·pending 재개로 `C` 가
 채택되는 길도 같은 답을 낸다.

## 18.3.1 ★그 감사 칸이 **지출을 발명했다** (Codex BLOCK, 같은 판)

`bgfirst.winner_origin` 을 더한 것만으로 결함이 하나 더 났다. 이 칸이
**없던 옛 기록**이 다음 방문에 그것만 얻으면, 생성·판정 0 인 **캐시
재사용**이 「기록이 움직였다」가 되어 JIT 재생성으로 세어지고 상한
래치까지 건다 — **게이트를 켜기 전에도** 난다.

스냅샷에서 **그 한 칸만** 뺀다. `bgfirst` 를 통째로 빼지 않는다 —
`chain_winner`·배경 경로·배경 자산은 **계속 견준다**(진짜 수정과 유료
흔적을 숨기면 안 된다).

★교훈: **감사 칸을 더하는 것도 공짜가 아니다.** 다만 **정확히** 적는다
 (Codex NON-BLOCK 정정) — 「배포만으로 전 샷이 지출이 된다」가 아니라,
 **`winner_origin` 이 없던 기존 bgfirst 완료 기록**이 JIT 재방문에서 그
 칸을 새로 얻으면 오계상한다는 것이다. 모든 샷·모든 칸 추가가 늘 그렇다는
 뜻이 아니다. 물어야 할 것은 「이 칸이 **지출 비교에 들어가나**」이고,
 `ref_mode` 를 `_presentation` 에서 빼 둔 이유가 같다.

## 18.4 ★여전히 비대상인 것

    · conti A/B — 두 갈래가 outer 선택 **전에** 각각 재롤하면 「샷당
      1장」이 아니라 **최대 2장**이 된다. 사용자 계약이 1장이다.
    · variants(4택1·2택1) — 이번 범위 밖이다. 막을 이유는 못 봤고
      후보 수만 맞추면 되지만, **안 본 것을 켜지 않는다**.

## 18.5 ★이미 ON 으로 끝낸 화는 **자동으로 다시 안 본다**

적용 범위를 넓힌 것이 **바깥 스텝 해시에 안 접혔다** — `image_steps` 도
`multiroll_select` 도 같은 `GATE_POLICY_VERSION` 그대로다. 그래서 게이트를
켠 채 이미 끝낸 화가 **확장 정책으로 저절로 재평가되지 않는다**.

지금은 운영 OFF 이고 **승인된 새 생성**에서 적용할 계획이라 이것이 결함은
아니다. 기존 ON 완료 화까지 적용하려면 **생성 지문이 아닌 단계**의 적용
범위 정책 신원을 따로 다뤄야 한다(Codex NON-BLOCK).

## 18.6 ★이 판에서 배운 것 — 우연히 맞아 안 걸렸다

「후보 수 = 롤 수 + 1」을 **3으로 박아 놓고도 다섯 시험이 다 통과**했다.
지금 네 갈래가 **전부 2롤**이라 우연히 맞았기 때문이다. 롤 수를 3으로
바꾼 시험을 더하고서야 빨간불이 났다 —
**같은 값이면 규칙을 안 잠근다.**

# 19. 발송 계측 ② — OpenAI **키 슬롯 경계** (2026-09-20)

`FailoverOpenAIClient._invoke` 가 gpt-image·responses 가 **나가는 자리**다.
슬롯 failover 가 그 loop **안**에서 돌아 **슬롯 진입이 둘**이 될 수 있다 —
그래서 loop 안에서 적는다.

★「논리 하나가 물리 둘」이라고 적지 않는다 (Codex NON-BLOCK). 이 자리가
 아는 것은 **슬롯 진입 수**다. SDK 안 재시도는 그 아래라 **실제 요청 수는
 미확인**이고, 아래 `slot_entry` 설명과 어긋나지 않게 이렇게 좁혀 쓴다.

    · 종류는 **호출 경로**로 가른다(`images.*` 인가). 문안을 훑어
      짐작하는 것이 아니라, 경로가 곧 그 호출의 신원이다.
    · 단위는 `slot_entry` — OpenAI SDK 자체 `max_retries` 는 **이 아래**라
      안 보인다. 「물리 전송 총수」라고 부르지 않는다.
    · 예외로 끝난 진입은 `failed` 다 — 「청구 안 됨」이 **아니다**.
    · 맥락은 소유자가 씌운 것을 **받아 쓴다**. 경계가 `work` 를 스스로
      짓지 않는다.

★이것으로 gpt 이미지가 장부에 들어왔다. 다만 **SDK 안 재시도**는 여전히
 미계측이다 — 그 아래를 재려면 transport/hook 계측이 필요하고, 그것은
 §17.6 의 다음 확장이다.
