# 고증 조사 계약 (GROUNDING-V2)

> 2026-08-29 Claude ↔ Codex 합의 · 사용자 확정. **구현 전 계약.**
> 종전에는 `artifact/20260829_grounding_contract/` 에만 있었는데 그 디렉토리는
> **gitignore** 라 fresh clone 에 없었다. 정본은 여기다 (Codex BLOCK-2).

## 왜 이 계약이 생겼나

「막차」 주행이 기술적으로 완주했는데 **그 시절을 겪은 사용자가 요금통·회수권이
전부 틀렸다**고 했다. 파이프라인은 「1983년」을 제대로 전달했지만
**1983년 회수권이 실제로 어떻게 생겼는지는 아무도 안 찾아봤다.**

### 실물로 확인한 결함 셋

| | 확인 |
|---|---|
| **① 죽은 코드** | 엔티티 시대 조사가 `reference_image_generator.py:239` 에 있는데 `generate_for_entity_with_retry` 호출이 `app/`·`tests/` 통틀어 **0건**, `ReferenceImageGenerator` 인스턴스화 0건. 현행 경로 `ref_image_pipeline.py` 에 `era` 라는 낱말 자체가 없다 → 저장소 전체 엔티티용 eraref **0장** |
| **② 순서 뒤집힘** | `description`·`t2i_prompt` 가 조사보다 **먼저** 상상으로 쓰이고 정본이 되어 참조 이미지로 굳는다. **조사가 묘사를 고치는 경로가 없다** |
| **③ 추출이 먼저 거른다** | `prompts/_base/entity_extract_v4/10.202607021935/prop.md` 「제외 기준」의 **「여러 번 생성했을 때 차이를 구분하기 어려우면 제외」**가 정확히 고증 대상을 거른다. 고정 설비·착용물도 제외. 저빈도 필터도 같은 방향 |

---

## 1. 세 갈래

「연구 대상 아님」이 「마음대로 지어내도 됨」으로 읽히는 것을 막는다.

| 갈래 | 무엇 | 처리 |
|---|---|---|
| **externally grounded** | 외부 기록의 정답이 시각 정본을 구속 | **조사** |
| **fictional / underspecified** | 외부 정답이 없는 창작물 | **디자인 저작** (조사 아님) |
| **generic / unconstrained** | 어떤 실제 변형을 골라도 서사 계약을 안 어김 | 일반 상세화 |

## 2. 판정 — 2×2

**가로축** = 이번 계획 렌더에서 틀렸을 때 **일반 관객이 알아보는가**
 = A(대다수가 구별 가능) **AND** B(구별 속성이 충분한 크기로 한 번이라도 보임)

### ★B 는 조사 시점에 **알 수 없다** — 확정값이 아니라 의도값이다 (Codex BLOCK)

실물: `framing_scale`(close/medium/wide/insert)은 **`shot_staging`(order 19.5)**
에서 처음 정해진다 (`prompts/_base/shot_staging/22.202608130110/system.md:67`).
조사는 `entity_detail`(14.0)보다 **앞**이어야 하므로 그 자리엔 framing 이 없다.

```
조사 경계에서   B 대신 visibility_intent 를 쓴다  (원문 + planned shot 근거)
                yes / no / uncertain — ★uncertain 은 조사한다 (보수적)
shot_staging    실제 framing_scale 이 나온 뒤 **사후 확인**
                의도와 어긋나면 진단으로 남긴다 (조사를 되돌리지 않는다)
```

★**「보일지 모른다」를 「안 보인다」로 닫지 않는다.** 조사 안 한 것은 되돌릴 수
없지만, 조사해 두고 안 보이는 것은 **시간만 쓴다** — 비대칭이라 uncertain 은 산다.

### ★프레임 점유 제한이 고증 소품을 못 보이게 한다
같은 프롬프트 `:264` 「단일 비인간 물체가 프레임의 **40% 이상 차지 금지**」·
「소품이 포커스 대상이라도 주변 환경과 함께 보여야 함」. 회수권·요금통처럼
**작은데 인쇄면이 핵심**인 대상은 이 규칙 아래서 알아볼 크기로 못 나온다.

**전역 삭제는 반대.** `research_required` 이고 `exact_variant`/`unique_identity`
이며 `visible_discriminators` 가 인쇄·표장·규격인 대상에만 **선택적으로** 완화한다.
★이 완화를 「§2-6 최종 A/B 통과 뒤」로 두면 **순환이다** (Codex BLOCK):
40% 제한 때문에 인쇄면이 안 보이면 **고증 개선을 관찰할 수 없어** A/B 가 못 이기고,
못 이기니 완화도 못 켠다. **갈라서 잰다**:

```
① 참조 층 A/B  ← 먼저.  ★참조 프롬프트에는 40% 제한이 **없다**
                        (`ref_image_prompts/5.202603311724/prop_ref.md:1`
                         「Object fills the frame」) — 지금 그대로 잴 수 있다
② 최종 스틸     3-arm:  legacy / grounded-only / grounded + 선택적 framing 완화
                        또는 staging 만 가르는 별도 A/B
```
①이 고증 자체를 판정하고, ②가 **framing 완화의 값어치를 따로** 판정한다.
자동 ON 은 여전히 반대 — 켜는 것은 ② 판정 뒤다.

### ★★★§2-5 · §2-6 의 **판정 주체** (2026-08-30 사용자 확정)

**이미지·고증 품질은 사람만 판정한다. VLM 을 평가자·판정자·랭커로 쓰지 않는다.**

| 단계 | **자동이 판정하는 것** | **사람이 판정하는 것** |
|---|---|---|
| §2-5 canary | 실행 무결성뿐 — 중복 구매 0 · resume 재구매 0 · 세 층 캐시 키 · rollback byte/provenance | 굽힌 canary 이미지가 **고증상 맞고 나아졌는가** |
| §2-6 눈가림 A/B | 순서 은닉 · arm 신원 · 기록 · 누락 방지 | **어느 쪽이 더 낫고 통과인가** |

★**VLM 점수·critique·다수결을 PASS 근거로 쓰지 않는다.** 자동화는 사람이
공정하게 볼 수 있게 판을 차리는 것까지다 — 판을 차린 것과 판정한 것은 다르다.

★단계 수는 안 바뀐다. **평가 주체만** 바로잡은 것이다.

### ★★세로축 `reliably_known` 은 **폐기했다** (2026-08-30)

옛 규칙은 이랬다:

```
(폐기)  고증 필요 = A AND B AND NOT(reliably_known)

세로축 = 이미지 생성 모델이 외부 grounding 없이 그 구별 속성을 맞출 근거가 있는가
| 알아본다 | 근거 없음 → 조사 | 근거 있음 → 조사 불요 (한복 · 무브랜드 90년대 차) |
```

**왜 폐기했나.** 그 축은 「**다른 모델**이 무엇을 그릴 수 있나」를 묻는다. 판정
모델은 그것을 본 적이 없다 — 계약 §3 이 스스로 금지한 자기평가다. 실측에서도
그 축만 흔들렸다: 축별 갈림 **5/6**, **한 모델 안에서만 봐도 4/6**, 두 판정자의
눈금이 한 칸 어긋난 자리가 정확히 route 를 자르는 자리였다(같은 대상에
Sol `easy`×3 · Gemini `medium`×3, **나머지 네 축은 여섯 표가 동일**).

### 지금 규칙

```
고증 필요   = A AND B          ← A 는 **출처로** 확정한다 (§4a)
엔티티 보존 = 고증 필요  OR  C(반복 등장 → 일관성)
```

★**A 도 분류기 답으로 쓰지 않는다.** 그 단계는 「검색을 하지 않는다」인데 세상
사실을 답하고 있었다. `grounding_class=generic` 으로 먼저 skip 하던 것도 같은
이유로 걷어낸다 — 「시대가 겉모습을 안 구속한다」도 **세상 사실**이다.

```
검색 전에 정하는 것 = fictional 여부 · referent_specificity · B   ← 원고에서 아는 것만

fictional                        → design
B=no                             → skip
현실 대상 + B=yes/uncertain      → §4a 조사
  출처로 시대 차이 확인          → research + reference
  출처로 「차이 없음」 확인       → skip
  미발견·충돌·출처없음·시간초과  → unresolved
```

★**「검색에서 아무것도 안 나옴 → no」는 금지**다. `no` 는 **긍정적 출처**가
있을 때만 나온다.

★**세 축은 여전히 별개다** — EntityCanon 존재 / research_required /
reference_required. 한복·무브랜드 차·자전거 정비소는 **엔티티로는 보존**되고
(반복 등장 일관성) **조사는 안 산다**. 옛 표의 「조사 불요」는 **조사 축만**
말한 것이지 엔티티에서 빼라는 뜻이 아니었다.

### ★REJECTED (2026-08-30) — VLM 으로 `reliably_known` 을 재려던 안

세로축 `reliably_known` 을 「target 이미지 모델이 참조 없이 구운 것을 VLM 이
채점해서」 발급하려 했다. **폐기한다.**

- **VLM 은 역사 정답의 권위가 아니다.** 1983년 회수권을 본 적이 없다.
  「종이에 번호가 인쇄됨」 같은 문장은 요즘 승차권도 만족한다 — 그 채점은
  관측이 아니라 **또 다른 짐작**이다. 사용자 판정: 「나도 모르는데 VLM 이
  어찌 알아」
- **정답 사진을 얻으면 이미 조사한 것**이다. 그러니 아낀 것이 없다.
  부류마다 한 번이라 아낀다는 논거도, 부류가 굵으면 그 판정을 다음 물건에
  적용하는 것이 다시 짐작이고, 부류를 좁히면 benchmark 수가 늘어 사라진다.

★이 절을 지우지 마라. 같은 안이 다시 올라오는 것을 막는 자리다.

### 「형태」를 실루엣으로 읽지 않는다
회수권은 **인쇄·재질·규격**, 제복은 **재단·색·표장·부속**이 핵심이다.
1번은 「형태」가 아니라 **「관찰 가능한 시각 속성」**이다.

### ★기존 제외 규칙과 글자만 비슷하고 정반대다

| | 묻는 것 | 무엇에 대한 물음 |
|---|---|---|
| 기존 제외 규칙 | 같은 프롬프트로 **여러 번 생성**했을 때 서로 비슷한가 | 생성 산출의 분산 = **모델 성능** |
| 이 계약 | 현대/default 물건을 놓으면 목표 시대의 **실제 물건과 시각적으로 틀리는가** | real-world counterfactual = **역사** |

**문안에서 반드시 갈라 적는다.**

## 3. 「모른다」를 「아니다」로 닫지 않는다

- 판정은 **yes / no / uncertain**. **uncertain 도 후보로 보존**한다 — 「몰라서 no」가 false negative 다
- 검색에서 증거를 못 찾으면 **no 가 아니라 `grounding unresolved`** — 상상 description 으로 내려가면 원래 결함이 돌아온다

### 「모델이 아는가」를 자기평가로 판정하지 않는다
같은 LLM 에게 「너 이거 아니?」를 묻는 것은 **조용한 false negative 경로**다.
이 절이 원래 두 층으로 나눠 두었던 bypass 규칙은 **위 §「세로축 `reliably_known`
은 폐기했다」로 통째로 폐기됐다.** 현행은 하나뿐이다:

```
고증 필요 = A(관찰 가능한 시대 차이) AND B(그 차이가 이 컷에 보이나)
            ★A 는 §4a 의 **출처 붙은 claims** 가 확정한다 — 판정자의 말이 아니다
```

<details>
<summary>★폐기한 옛 규칙 (2026-08-30 이전) — 역사 기록. <b>따르지 마라</b></summary>

1. generic·비특정 클래스는 provider **capability 근거**가 있을 때만 bypass
2. exact referent 는 모델의 말만으로 bypass 하지 않는다
3. capability 는 이미지 provider+model+version 에 귀속, 모델이 바뀌면 다시 uncertain
4. 초기 판정자 **Sol** 단독, 절단선 `매우 쉬움/쉬움 → 건너뜀, 보통 이상·uncertain → 조사`

**왜 폐기했나** — ①`capability`(=`difficulty`)는 판정 모델에게 **다른 모델**이
무엇을 그릴 수 있나를 묻는 자기평가라 이 절 자신의 금지를 어긴다. 실측에서 그
축이 판정자 사이 5/6, **한 판정자 안에서 4/6** 갈렸다. ②판정자는 이제 **둘**
(`gpt`+`grok`)이고 단독 Sol 이 아니다.

</details>

### `referent_specificity` enum (SOT)
```
generic_class    한복 · 무브랜드 90년대 차
branded_family   특정 모델 없는 나이키
exact_variant    2020 쏘나타 · 특정 연식·계급 제복      → 조사 강제
unique_identity  알려진 인물 · 명소 · 유물               → 조사 강제
```
판정자 출력에 함께: `confidence` · `visible_discriminators` ·
`likely_failure_modes` ·
`judge_step`/`judge_model_alias`/`judge_physical_model`
(★`judge_model`·`judge_model_version` 이 아니다 — 판정자를 둘로 늘리면서 셋으로
갈랐다. 부탁한 alias 와 **응답이 말한** 물리 모델을 따로 남긴다).
**최종 route 는 코드가 이 필드로 결정한다. 필드끼리 모순이면 조사.**

★★`difficulty`·`discriminability`·`target_image_provider/model/version` 은
**2026-08-30 에 전부 걷어냈다** — schema·지시문·record·지문·planner required·
접기 표까지. 「폐기 대상이지만 아직 코드에 남아 있다」던 절이 여기 있었는데,
그 절이 남아 있는 동안 팩만 새 계약이고 **실제 user prompt 는 옛것**이었다
(Codex). `CLASSIFIER_CONTRACT_VERSION=3` · `PLANNER_CONTRACT_VERSION=8`.

`target_image_*` 를 같이 뺀 이유: 그 좌표는 폐기한 `difficulty`(「이 이미지
모델이 근거 없이 맞힐 수 있나」)의 **전제**였다. 축이 없어진 뒤에도 지문에
남기면 이미지 backend 만 바꿔도 같은 분류를 **전부 다시 산다**.

### ★★어긋난 것 — 「전부 걷어냈다」는 **지금 사실이 아니다** (2026-08-31)

위 절은 08-30 **낮**의 상태다. 같은 날 **저녁**에 `generation_difficulty` 를
되살렸는데(팩 15) **이 문서를 안 고쳤다.** 계약과 코드가 서로 반대로 말하는
자리이므로 여기 적는다 — 「같은 규칙을 두 곳에 적으면 한쪽만 고쳐진다」.

되살린 까닭: `difficulty` 를 지웠더니 **엔티티 등록 예외의 유일한 문**이
같이 없어졌다. 사용자가 확정한 두 축 중 **둘째**(「한 번만 나와도 만들기
어려우면 등록」)를 열 것이 아무것도 안 남는다.

    지금 살아 있는 문 (전부 `generation_difficulty` 를 탄다)
      grounding_overlay.py:193-196   needs_reference_acquisition 로 대상 선별
      entity_steps.py:583-614        그 대상을 **실제 엔티티 행으로 만든다**
      reference_acquisition_step.py:58-62   참고 사진 대상

#### ★★★이것은 **한시적 다리**지 정본이 아니다

★한 판 앞서 나는 여기에 「폐기한 `difficulty` 와 **같지 않다**」고 적었다.
**틀렸다** (Codex 2026-08-31). 값이 3개인 것·route 를 안 가르는 것·추가 호출이
0인 것은 **표현과 비용과 소비처**의 차이일 뿐이고, **판정하는 뜻**은
`era_research.assess_subjects` 와 **같다** — 「이미지 모델이 틀리기 쉬운가
그리고 사람이 알아채는가」. 뜻이 같으면 두 벌이다.

같은 판에 적은 다른 두 문장도 틀렸다 —

    ✗ 「`subjects.maxItems = 1` 이라 엔티티마다 판정을 못 낸다」
      → `screen_subjects` 가 **대상마다 한 번씩** `assess_plan_cached` 를
        부른다(`grounding_screen.py:234,245`). 호출 하나가 대상 하나를
        flag 하는 계약과 정확히 맞는다. `maxItems=1` 은 **기능 불능**이
        아니라 **대상당 호출 N개**라는 비용 구조의 증거다.
    ✗ 「assess 는 조사 구매, `generation_difficulty` 는 등록·참조라 묻는 것이
       다르다」
      → **판정과 효과를 섞었다.** 판정은 하나이고 그 의무를 등록·보호·참조
        획득 **여러 소비자**가 쓴다. `hard/not_hard` 별도 칸도 필요 없다 —
        envelope 의 `plan`/`no_subject`/`failed`(`era_research.py:290,663,692`)
        와 screen 의 `obligation`/`not_target`/`unresolved`/`capped` 가 이미
        가른다.

그러므로 `generation_difficulty` 의 지위는 이것뿐이다 —

    **§3.5 원자적 cutover 전까지의 한시적 다리(compatibility bridge).**
    **정본(SOT) 이 아니다.** 판정 정본은 `era_research.assess_subjects` 다.

#### 제거 owner 와 resume 조건

★먼저 지우면 문이 **0벌**이고, screen 만 먼저 켜면 정본이 **두 벌**이다.
그래서 ⓑ와 ⓒ를 따로 켜 두지 않고 **한 번에** 넘긴다 (Codex 확정) —

    원자적 cutover — 아래를 **한 판에** 같이 한다
      · `grounding_screen` 을 `if_grounding_v2` 로 활성화
      · `entity_filter` 가 `grounding_screen` 에 dep 를 걸고
        obligation ID 로 등록·보호
      · reference acquisition 도 **같은 obligation ID 만** 소비
      · 동시에 `generation_difficulty` 를 classifier 팩·schema·prompt·
        record·지문·planner required/enum·기존 소비자에서 제거
      · schema/config stamp 무효화
      · 끝점 확인 — 「1회 hard+notice 등록/보호/획득 · not_target 미등록 ·
        unresolved 하류 차단」

★**그 앞에 결정할 것이 하나 더 있다.** 지금 screen 은 **대상당 호출 N개**다.
사용자가 반복 분석·토큰 구조를 다시 보라고 했고, C(c)는 **mock 계약만**
승인됐지 production orchestration 은 아직이다. 그래서 **§3.5 에서 지금 모양의
per-subject screen 을 바로 켜는 것도 아직 승인 안 됐다** — 구간마다 1콜 /
병렬 chunk + merge 쪽 생산 구조를 먼저 정한다.

★**§2 의 통과 조건(계획 392줄)에는 `difficulty` 의 「부재」가 없다.** 그래서
이 한시적 다리를 여기 정확히 적어 두면 §2 자체는 통과다 — §3.5 는 별도 보류.

## 4. 「개별 조사 불요」는 「시대 적용 불요」가 아니다

> 관찰 가능한 차이가 없으면 **그 엔티티에 대해 별도로 획득할 고증 사실 자체가 없다.**
> 시대 분위기는 **location/world grounding 이 계속 담당한다.**

★**그리고 시간은 정당한 우회 사유다** (사용자 확정). 「AI 가 쉽게·맞게 만드는 것은
**생성 시간 때문에** 조사에서 제외해도 된다」 — 한복·무브랜드 90년대 차·무브랜드 PC.

★★다만 **그 우회를 판정 모델에게 물어서 정하지 않는다** — 그것이 폐기된 축이다.
시간을 아끼는 자리는 이제 **`plan_admission` 의 상한**이다: 무엇을 살지 결정적
순서로 고르고, 상한을 넘은 것은 `unresolved`+`time_capped` 로 서서 **다음 판에
다시 산다**. 「쉬움이라 건너뛴다」가 아니라 「이번 판에는 여기까지 산다」이다.

★그리고 **한복이 조사에서 빠지는 것과, 한복이 엔티티·아웃룩에 남는 것은 별개다**
(사용자 2026-08-30). 계약 §7 「세 축은 별개」를 보라.

중앙 클로즈업의 특정 관공서 의자처럼 **정체성이 생기면 엔티티로 승격**된다.

## 5. 조사 순서 (사용자 지시)

```
텍스트 조사 → 정식 명칭·연식·변형·시각 구별점·검색어 확정 → 이미지 검색 → 근거 묶음
```
**무엇을 이미지 검색해야 할지 모를 수 있으므로** 텍스트가 먼저다.
텍스트 단계가 못 풀면 **「쉬운 대상」으로 내려가지 않고 grounding unresolved**.

### 조사 산출물은 「사진 1장」이 아니다
1. **출처가 붙은 사실 / 금지 사실** — 형태·재질·구조·시대 범위. 「무엇이다」와 **「무엇은 아니다」**
2. **그 사실을 시각화할 참조 사진** — 사실의 대체물이 아니라 보조

★이번 L01 참조의 선정 사유가 **「요금통은 보이지 않으나…」**였다.
**그 사진 한 장으로 요금통 정본을 만들 수 없다는 증거가 이미 있다.**

## 6. 세 축 · 이미지 강제

| 축 | 뜻 |
|---|---|
| `EntityCanon` 존재 | 정본 ID 와 provenance 를 가진 데이터로 보존 |
| `research_required` | 외부 기록으로 구속되는가 |
| `reference_required` | 독립 참조 이미지를 굽는가 |

★**`research_required=true` → `reference_required=true` 강제** (사용자 지시).
역은 아니다 — 반복 등장 C · 캐릭터 identity · 기존 일관성 계약도 독립적으로 만든다.

★**조사 실패 때 추측 이미지를 만들라는 뜻이 아니다.** 완료 조건은
**텍스트 근거 + 이미지 근거 + 생성된 정본 reference** 셋이고, 못 얻으면 **unresolved fail-closed**.

사용자의 **「무조건 엔티티」**는 **정본 ID + 조사 출처를 가진 데이터 엔티티로 보존**한다는 뜻.

### ★현행 reference 정책이 이것을 정면으로 막는다 (Codex BLOCK · 실물 확인)

```
episode_reference_policy.py:61   character/nonhuman  visible_shot_count < 2 → text_only
                          :80   prop 등              visible_shot_count < 2 → text_only
```
**선택 샷 기준 저빈도면 강등한다.** 1983 회수권처럼 **한 번 크게 나오는** 고증 대상이
정확히 여기서 죽는다. 계약상 **`research_required` 가 이 빈도 규칙보다 우선**한다.

### ★참조 생성이 현재는 고증을 **증명하지 않는다** (Codex BLOCK · 실물 확인)

```
ref_image_pipeline.py:322        타입별 일반 템플릿에 entity_description 만 넣는다
                                 (claim·근거 이미지·금지 사실이 안 들어간다)
검증기                            description ↔ 결과만 대조 — 조사 주장을 안 본다
reference_phase1_service.py:174  "prompt_used": t2i  ← **입력 t2i** 를 저장한다.
                                 실제 provider 에 보낸 current_prompt 가 아니다
```
그래서 지금은 「조사가 그림에 닿았나」를 **기록만으로 되짚을 수 없다.**

V2 의 reference 생성·검증은 다음을 **함께 전달하고 저장**해야 한다:

| 무엇 | 왜 |
|---|---|
| sourced claims + **금지 claims** | 생성이 조사를 소비했음을 증명 |
| 검색 근거 이미지 | 정본 참조의 재료 (★정본 참조 자체가 아니다) |
| **실제 provider 에 보낸 최종 prompt** | 지금은 입력 t2i 를 대신 저장한다 |
| 소비한 **research revision / content hash** | 무엇을 보고 그렸는지 |

★**저장은 additive-only.** `prompt_used` 의 뜻을 바꾸지 않고 **새 칸에** 실제 prompt 를
넣는다 — 안 그러면 rollback acceptance 의 baseline 비교가 함께 흔들린다.

### ### ★활성 ref 프롬프트에 **시대라는 낱말이 0건**이다
실측: `prompts/_base/ref_image_prompts/5.202603311724/prop_ref.md` (활성 최신)에
`시대`·`era`·`period` 가 **한 건도 없다**. 조사 결과가 참조 이미지까지 올 통로가
프롬프트 층에도 없다 → **v2 전용 `ref_image_prompts` 버전**이 필요하다.

### ★owner 를 prop 으로 억지로 만들지 않는다
현행 reference producer 와 gate 는 `location`·`outlook` 을 **대상에서 뺀다**.
배경·아웃룩 고증을 하려면 **`location_part` 와 `outlook facet` 을 정식 owner 로
추가**한다. base location 을 prop 으로 우회 등록하는 것은 **금지**.

## 7. 단계 소유권 · 삽입 자리

```
추출 (entity_all / entity_extract)
    A·B(+uncertain) 로 후보 표식만. 원문 근거 함께.
    ★기존 제외 규칙보다 먼저 건진다 — 별도 통로

canonical merge / sync           정본 ID 확정 · 중복 제거
★통합 entity research            ← 여기. 새 경계
    planner 가 research_required 확정 · 실제 조사
    「이번 렌더에서 보이는가」는 여기서 본다
    false 면 강등 말고 「grounded candidate, deferred」

entity_detail / t2i              조사 결과를 소비만. required 인데 미완료면 못 내려간다
reference 생성
```

★**raw extraction 후보마다 바로 검색하면 merge 전 중복 신원을 여러 번 산다.**
그래서 **merge 보다 앞당기지 않는다.**

### ★★조사 경계에 `canon_id` 가 **없다** — 먼저 계약으로 고정할 것

실물 확인:
- `EntityMergeStep`(13.5)은 **short_id 중복만 제거**하고 **DB canon 을 만들지 않는다**
- `EntitySyncService` 는 **`entity_t2i`(15.0) 체크포인트를 읽어** canon 을 upsert 한다
  (`entity_sync_service.py:30` — `self._load_cp("entity_t2i")`)

즉 **13.7 뒤 경계에 canon_id 가 있다는 내 전제는 틀렸다.**
durable research/cache key 를 canon 으로 만들 수 없다.

★**닫았다 — 계획 §6.** ㉠(identity-only sync)은 legacy canon 생성 시점을 바꾸므로 기각.
㉡의 **destructive re-key 도 아니다**. A0/A 가 **append-only provisional
`research_subject_id`** 를 발급해 조사의 owner 로 삼고, 15.0 sync 뒤
**subject_id → canon_id 를 append-only 로 bind** 한다. 임시 owner 는 지우지 않고
provenance 로 남는다. ★**research row key 와 cache key 를 가른다** —
`facet·era·region·pack hash·target model` 은 **cache scope 지 identity 가 아니다**.
★정규화 name 은 identity 가 **아니다**: `entity_canon` 의 유일 색인은
`(project_id, short_id)` 하나뿐이고 name 제약이 없다. 모호하면 **`unresolved`**.

### C(반복 등장) — **닫음**: planned `shot_count`
- `entity_all_*` 가 `shot_count` 를 만들고 `EntityFilterStep`(order 13.7)이 **복원한다**
  (`entity_steps.py:353` 「entity_all에서 shot_count 복원」) → **조사 경계에 이미 있다**
- 없는 것은 **selected-shot count** 다 (`shot_selection` 은 order 15.5)
- ★**C = planned `shot_count`** 를 보존 기준으로 쓴다. selected count 는 **planner 우선순위로만**.
- 현→목표 그림의 **정본은 계획서 §1.8** 이다. 이 문서는 링크만 한다.

## 8. 겹치지 않게 · 덮어쓰지 않게

- 조사 대상에는 **canonical key 와 owner 가 하나**. 엔티티 소유(이름 있는 소품·유물·장치·
  브랜드·명소·제복) / 장소 scope 소유(엔티티가 아닌 잔여 환경 재질·set dressing)
- 씬 조사기가 구체적 물건을 발견하면 **새로 검색하지 말고** canonical entity research 참조
- 캐시 키 = **canon/variant + era + region + facet + research-pack hash**
- 조사의 집은 **별도 durable research record**. `EntityCanon` 평문 칸이나 `review_notes` 반대 —
  이번에도 **검증 결과가 그 칸을 덮어 감사 기록이 사라졌다**
- ★`EntitySyncService:165-174` 가 **이름으로 canon 을 찾아 매 에피소드 덮는다.**
  context 별 형태는 **variant canon** 이어야 한다

## 9. 인물

「인물은 전부 identity 관할」이라는 **일괄 제외는 틀렸다.** facet 으로 가른다.

| facet | 관할 |
|---|---|
| 허구 인물의 얼굴·골격 | 기존 identity |
| 시대 복장 · 직업 제복 · 군복 · 머리양식 · 분장 | **조사 대상** (CharacterOutlook / visual variant) |
| 실존 역사 인물 | **base identity 자체가 조사 대상** |

## 10. 기존 프로젝트

- 전 프로젝트 자동 소급 ✕ · 새 프로젝트만 ✕ — **둘 다 부적절**
- 기존 canon 은 `research_status = unknown / legacy`. **기존 이미지·기록은 안 지운다**
- 새 이미지 생성/재생성 때 **필요한 엔티티만 lazy backfill**.
  research → detail → t2i → reference → 의존 scene 순으로만 stale
- 열람은 막지 않되 **legacy unknown 으로 새 이미지를 계속 굽게 두지 않는다**
- **「막차」 프로젝트는 명시적 backfill 대상** (요금통·회수권 오류 확인됨)

## 11. 사람은 참여하지 않는다

최대한 자동화. 수동 개입은 나중에 **단계 진행 시 직접 수정(이미지 수동 입력)** 형태로 별도.
「사람 확인 관문」은 **기각**.

### ★그런데 사람이 이미 정본을 직접 고칠 수 있다 — **resolver 문구만으로는 안 된다**

처음엔 「사람이 이기고 충돌은 resolver 에서 푼다」로 닫았다. **부족하다** (Codex BLOCK · 실물 확인):

```
api/v1/entities.py:226            PATCH 가 canon 의 name·description·stable_traits·t2i_prompt 를
                                  **표식 없이 직접 바꾼다**
entity_sync_service.py:170-174    다음 sync 가 existing.description / t2i_prompt /
                                  stable_traits 를 **무조건 다시 덮는다**
같은 함수                          existing 을 **이름으로 찾는다**
                                  → 사람이 이름을 바꾸면 다음 sync 가 **새 canon 을 만든다**
```

즉 **사람 수정은 지금도 다음 주행에서 조용히 사라진다.** resolver 는 무엇이 사람 것인지
알 방법이 없다 — **provenance 는 별개 결함이 아니라 resolver 의 필수 입력**이다.

**닫는다 — 셋 다 필요하다**

| # | 무엇 |
|---|---|
| 1 | `PATCH` 를 **canon 직접 변경이 아니라 append-only field override** 로 재배선한다. `actor` · `time` · `supersedes` · `clear` 를 **SOT 로 기록**한다 |
| 2 | **이름을 가른다** — 사람이 바꾸는 것은 **`display_name` override field**, sync 가 쓰는 **identity 는 따로** 둔다. ★기존 `entity_alias` 를 표시명으로 전용하는 것은 **기각** — 그 테이블의 현재 뜻은 **prop term-match 동의어**이고 API 응답 이름은 `canon.name` 에서 나온다 (계획 §7) |
| 3 | resolver 우선순위 = **사람 override > 완결된 v2 revision > legacy canon**. v2 revision 은 **지우지 않고** 진단으로 남는다 |

**끝점 시험 넷** (조립부 말고 DB 칸·API 응답에서 잰다):
```
① PATCH → legacy sync 한 판 → 사람 값이 **그대로 있다**
② 그 뒤 새 v2 revision 이 생겨도 노출은 **사람 값** (revision 은 shadow 로 남음)
③ override 를 clear 하면 **v2 값이 드러난다**
④ 사람이 이름을 바꾼 뒤 sync → **중복 canon 0개**
⑤ PATCH 로 이름을 바꾸면 list·get 이 **사람 표시명**을 내고 `canon.name` 은 **불변**
```

## 12. 모드 — 상호배타 enum

```
legacy        현재 scene_image_pipeline 만 호출
shadow_plan   새 분류·질의계획만 만들고 검색/이미지 생성 0
v2            통합 경계에서 **canonical entity owner 와 residual location/world owner 를 모두** 처리.
              ★금지하는 것은 **옛 scene_image_pipeline 호출**이지 location scope owner 자체가 아니다
              (엔티티가 아닌 잔여 환경 재질·set dressing 은 계속 location/world 소유)
```
boolean 두 개면 **둘 다 켜져 두 번 사는 상태**가 존재한다. enum 이면 구조적으로 불가능.
★**지문 접는 자리를 갈라야 한다** (Codex BLOCK):
- `shadow_plan` 은 **shadow checkpoint 지문에만** 넣는다. production 지문에 접으면
  **검색도 산출도 안 바꾸는 무료 관찰을 켠 것만으로** detail/t2i/reference/scene 이 stale 된다
- v2 하류에는 **mode 문자열이 아니라 실제 소비한 research revision / content hash** 를 넣는다
- `legacy` 복귀 시에는 **legacy resolver 입력만** 접혀야 한다 —
  그래야 v2 산출을 재사용하지 않으면서 baseline 을 되살린다

팩 · **판정자 좌표(부탁한 alias 와 응답이 말한 물리 모델 — 지금은 `gpt`+`grok`
둘이다. 「Sol 버전」이 아니다)** · target image provider/model/version ·
research schema 는 각 층 지문에 넣는다.

---

## 13. ★조사 뒤에 외형을 **다시 짓는** 자리 — owner 제한 (Codex BLOCK)

고증 revision 이 생겨도 **뒤쪽 여러 단계가 외형을 새로 저작한다.** owner 규칙이
없으면 조사 결과가 그대로 덮인다. 실물로 확인한 자리:

| 자리 | 지금 무엇을 저작하나 |
|---|---|
| `outlook` phase1 | 원문에 복장 묘사가 없어도 **추론해 구체 지정** |
| `outlook` phase2 | 배정이 없으면 **다른 인물의 유사 복장을 빌려** 준다 |
| `background_prompt` v14 | `visual_world_rules` 만으로 **시대 건축·설비·차량을 발명** |
| `scene_detail` v47 | 등록 안 된 common noun 을 허용 |
| `visual_continuity_anchor_plan.py:638` | `printed_content`·`physical_form`·`scale_contract` 를 저작하고 **ref/detail prompt 에서 canon t2i 보다 앞선다** (canon 자체는 안 덮는다) |

### 권위 표 — 이 네 줄이 전부다

| 축 | owner |
|---|---|
| **WHAT · COUNT** (무엇이 몇 개 있나) | 원문 · A0 |
| **LOOK · FORM · MATERIAL** (어떻게 생겼나) | **grounding revision** |
| **WHERE · STATE** (어디에 어떤 상태로) | staging · continuity anchor |
| 엔티티가 아닌 잔여 set dressing | 기존 legacy 자유 생성 |

### ★gate 를 거는 대상 = `grounding_controlled` (research_required **만이 아니다**)

처음엔 「`research_required` 대상에만」이라고 적었다. **구멍이 있다** (Codex BLOCK):
그러면 **uncertain** 과 **조사 실패 unresolved** 가 빠져서, **claim 을 못 찾은 바로 그 순간**
outlook·background·VCA 가 외형을 다시 지어낸다. 계약 §3(「모른다를 아니다로 닫지 않는다」)과
정면으로 어긋난다 — 내가 스스로 어겼다.

```
grounding_controlled = research_required  ∪  uncertain  ∪  unresolved
generic              = 나머지 — 기존대로 자유 생성
```

★`grounding_controlled` 대상에 대해서는 위 네 단계가 **LOOK/FORM/MATERIAL 을 저작하지
못한다.** VCA 의 `printed_content` 는 회수권 인쇄면 그 자체라 **정확히 grounding 소유**다 —
v2 에서는 VCA 가 **조사 claim 을 읽어 채우고**, 없으면 `unresolved` 로 서지 저작하지 않는다.

★**generic 대상에는 이 제한을 걸지 않는다.** 전역으로 걸면 일반 소품이 전부
`unresolved` 로 서서 파이프라인이 멈춘다.
★그리고 `grounding_controlled` 인데 claim 이 없으면 **저작이 아니라 `unresolved` 로 선다** —
「없으니 지어내라」로 내려가는 길을 막는 것이 이 gate 의 전부다.
