# 참조 획득 일원화 — **이미 있는 것을 앞으로 옮긴다**

> **사실 정리 문서다.** 오늘 확인한 것을 적는다. 미결은 미결로 적는다.
> 결정이 나면 이 문서를 고치고 그때 구현한다.

## 0. 사용자 확정 (2026-08-30)

1. **기존 엔티티 추출 축은 그대로.** 「2번 이상 나오면 중요 엔티티」
2. **예외를 얹는다** — **이미지 모델이 만들기 어려운 것**(옛 화폐 · 옛 특정
   브랜드 제품 · 잘 알려진 장소)은 **한 번만 나와도 엔티티에 등록**
3. 그 엔티티는 **검색 → 이미지 검색**을 탄다. 사전 검색은 판정이 아니라
   **「무엇을 찾을지」 자료 모으기**
4. 이미지 검색 결과 **3~5장을 한꺼번에** VLM 에게. **없으면 좁혀서 한 번 더**
5. ★**VLM 은 「우리가 찾던 종류가 맞나」만** — 자동차인지 화폐인지.
   **「상세히는 인간도 몰라, 전문가가 아니면」**
6. ★**다섯 갈래 전부** 거쳐야 한다 —
   `prop` · `character` · `location` · `location_part` · `outlook`
7. ★**새로 만드는 것이 아니다.** 뒤쪽(최종 이미지 생성 근처)에서 하던 것을
   **앞으로 옮기고 뒤에서는 뺀다**

## 1. ★그 「이미 있는 것」은 `era_research.py` 다

2026-08-14 사용자 확정. 문서 첫 줄:

> 「그냥 이미지 생성으로는 그 시대나 장면을 제대로 묘사하기 어려운 경우
>  **서울역과 같이 사전 이미지 조사가 필요해, 무조건**」
> 「서울역 재생성(`regen_period_bg`)에서 통한 구조(검색→후보→VLM 선택→참조)의
>  **범용 정식 배선**」

### 그 팩(`3.202608251500/assess_sys.md`)이 이미 묻는 것

```
Flag a subject ONLY when both are true:
1. 텍스트만으로는 이미지 모델이 물리 형태·부속·양식을 틀릴 만하다
   (화폐와 보안 요소, 브랜드 가전·신발·패션, 차량, 대중교통 내부,
    가전, 제복, 특정 시대 점포 집기 …)
2. 그곳 그 시대의 보는 사람이 그 잘못을 알아챈다 —
   그 형태가 공개적이고 알아볼 수 있는 지식이다
```

①이 사용자 확정 2, ②가 **「보는 인간이 쉽게 엉터리임을 안다」** 그대로다.
그 밖에 이미 있는 것: 「대부분 빈 목록」 · 「통칭 금지」 ·
「한 장이 혼자 보여줄 수 있는 하나 · 순서가 우선순위」 · 원어 질의 + 잠금문.

★그래서 내가 오늘 새로 지은 판별 축(`generation_difficulty`)은 **중복이고
그쪽이 더 낫다** — enum·문턱·등급이 애초에 필요 없다.

## 2. 뒤쪽에서 **사라져야 하는 네 자리**

| 자리 | 무엇 | 대상 |
|---|---|---|
| `reference_image_generator.py:255` | B1 엔티티 참조 | `prop`·`location` **만** |
| `still_recipe_service.py:2159` | B2 groupbg 플레이트 | 장소 |
| `still_recipe_service.py:4152` | confined 샷 look 앵커 | 샷 |
| `still_recipe_service.py:4477` | 샷별 배경 판 | 샷 |

★그 코드 **주석이 왜 앞으로 옮겨야 하는지를 이미 적고 있다**:

- `:4477` — 「샷별 배경 판 — 이 주행이 실제로 재생성하는 배경 경로(**실측
  98/215**)인데 era 조사가 groupbg(B2)에만 있었다」 → 자리마다 덧붙여 온 이력
- `:2159` — 「직호출 판별+조사는 **재방문마다 검색을 다시 돌려** 회수 사진이
  그때그때 다르고, 그 sha 가 지문에 접혀 **완성된 배경을 통째로 재생성**시켰다」
  → **뒤에서 사기 때문에 난 사고**. 앞에서 한 번 사면 원인이 없어진다

## 3. ★다섯 갈래 — 막는 것은 **대상이 생기는 시점**이다

| 갈래 | 엔티티 행이 생기는 자리 | 13.8 에서 |
|---|---|---|
| `prop` · `character` · `location` | `entity_merge` 13.5 → `entity_filter` 13.7 | **있다** |
| `outlook` | `outlook_phase1` **19.0** | **없다** |
| `location_part` | ★**chunk producer + EntitySync 13.5** | **있다**(2026-09-01) |

★**2026-09-01 갱신 — 이 줄이 바뀌었다.** `location_part` 에는 이제 producer 가
있다(§2-6.5a, tip `19730ca2`): 제 갈래(`LP##`)로 `entity_canon` 에 등록되고
`part_of` 가 `RelationFact` + participant(part/whole)로 **DB 에 남는다**.
`build_subjects` 가 `location_parts` 를 **스캔**하므로 이미 등록된 LP 행은
`binding=entity_row` 로 붙어 **고증 대상까지 간다**.

★그래도 **일반 승격은 안 한다**: 행도 부모도 없는 LP 후보는 `deferred` 로
남는다(`GENERIC_PROMOTION_OWNERS = C·L·P`). 부모 없이 만들면 고아 LP 다.
새로 만들려면 `LP##` + 정확한 `part_of` 부모를 함께 내는 **parent-aware**
경로여야 한다 — 그것은 아직 없다.

앞서 「미루는 주석뿐」이라던 세 곳 중 `grounding_carry`·`grounding_overlay` 는
**계약으로 바뀌었고**, `episode_reference_policy_step` 은 여전히 `C`/`P` 만
받는다(`REFERENCE_SUPPORTED_OWNERS`) — 중앙 획득 배선 때 함께 연다.

### ★확정 (Codex 2026-08-30) — **두 단계, 획득은 한 곳**

내가 낸 「스텝 여럿」 안은 **채택 안 됐다.** 이유가 정확하다 —
`entity_detail`·`entity_t2i` 는 **선택된 웹 참조를 소비하지 않는 텍스트
단계**다. 그래서 13.8 에서 획득 owner 를 둘로 쪼갤 이유가 없다.

```
앞쪽 (entity_filter **전**)
  · assess_subjects 판별
  · 저빈도 보호 (hard 예외를 기존 통로에 union)
  · base materialization (prop·character·location)
  · **원어 검색 질의 · 언어 잠금문 저장**
        ↓
        …  outlook_phase1~3 · background_classify · §2-6.5 facet producer
        ↓
중앙 획득 (약 **19.x 한 곳**) — 모든 실제 대상 producer **뒤**,
                                이미지 생성 소비자 **앞**
```

★`outlook` — A0 단계에서 **획득 의무를 먼저 보존**하고, `outlook_phase1~3` 가
실제 row 를 만든 뒤 **그 row 에 결속**한다. 한 번 나온 어려운 아웃룩이
producer 입력에서 빠지지 않도록 **보호 의무를 넘긴다**.

★`location_part` · outlook **facet** — 다른 base 로 **억지 승격하지 않는다**.
§2-6.5 facet producer 가 stable row/id 를 만들게 하고, **그 producer 를 중앙
획득의 선행조건으로 올린다**. §2-6.5 없이 미뤄 둔 채 「완료」라 부르면 안 된다.

★§2-6.5 를 그 앞으로 못 옮기면 **같은 모듈·계약·장부를 쓰는 증분 wrapper 만**
허용한다. 구현·캐시·프롬프트가 두 벌이면 안 된다.

## 4. ★★건드리면 안 되는 것 — 실측이 코드에 박혀 있다

| | |
|---|---|
| 판별 모델 `gemini-flash` | 「같은 팩·같은 입력에 모델만 바꿔 재니 gpt 는 **17/17** 을, gemini-pro 는 **6/8** 을 「비대상」(빈 목록)으로 냈다. 둘 다 파싱 실패가 아니라 실제 판단이라 **바꾸면 조사가 사실상 꺼진다**」 |
| 캐시 계약 | 키=대상 텍스트·세계관·정책·팩 sha16 · **빈 목록(비대상)도 캐시**(재판별 지출 차단) · **실패는 캐시 안 함** · `failed_memo`(한 걷기 안 재시도 금지) · 정본 신원 셋이 **다 있어야** 합침(「**잘못 합치는 것이 중복 조사보다 나쁘다**」) · `era_fail::` attempts **단조 증가**(안 그러면 무상한 재검색이 열린다) · 참조는 `eraref_<sha16>.png` 내용 주소화 |

## 5. ★현재 `era_research` 의 **결함 하나** — VLM 이 너무 많이 판정한다

`research_reference` 는 선택에 `search_grounded_ref` 의 `pick_system` 을
**그대로** 쓰고, `build_pick_user_head` 가 심판에게 **시대·지역을 준다**:

```
THE STRUCTURE the photograph must show: …
REGION AND ERA it must belong to: …        ← 심판에게 준다
```

`pick_system` 은 그걸 받아 판정한다:
「**다른 나라나 다른 시대면** 탈락」 · 「관광지·랜드마크·꾸민 것·특이 디자인이면
탈락」 · 「**방문객용으로 개조됐는가**」

★**사용자 확정 5 와 정면으로 어긋난다.** 시대·지역 좌표는 **검색 질의에는
남기고 심판에게는 주지 않아야** 한다. 그래서 오늘 지은
`coarse_type_pick`(종류/가시성만)은 **중복이 아니라 정정**이다.

★`combine_pick_verdicts` 의 **0~100 평균 최고점** 선택도 같은 부류다.
실측에서 Gemini 가 0~100 을, GPT 가 사실상 0~10 을 써서 평균이 한쪽에
지배됐다(주유소 그룹 100 대 10). `era_research` 는 **GPT 단독 1심**이라
덜 드러날 뿐 계약은 여전히 점수 기반이다.

## 6. 오늘 지은 것 — 남길 것과 버릴 것

| | 판단 | 왜 |
|---|---|---|
| `generation_difficulty` 축 | **버림** | `assess_subjects` 가 판별+원어 질의를 1콜로 더 낫게 한다 |
| `coarse_type_pick` 팩·combine | **살림 — 정정** | 그쪽은 시대·국적·평범함까지 묻는다 |
| 라운드 계약(좁혀 한 번 더) | **살림** | `era_research` 에 **재검색이 없다**(`MAX_CANDIDATES=4`, 실패하면 그대로) |
| `materialize_missing_entities` | **살림** | 그쪽에 없다 — 이미 만들어진 엔티티에만 붙는다 |
| `acquisition_contract_sha` · `acquisition_owner` | **살림** | 앞뒤를 잇고 이중 구매를 막는다 |

## 7. ★확정 (Codex 2026-08-30) — 미결이 닫혔다

| 물음 | 확정 |
|---|---|
| 판별 주체 | **`era_research.assess_subjects` + `gemini-flash` 단일 SOT.** 별도 `generation_difficulty` enum/schema/지문/route 는 **버린다** — 같은 유료 판단을 두 번 사면 안 된다 |
| 판별이 subject 를 내면 | **저빈도 보호 + 참조 획득 의무**가 생긴다. 빈 목록이면 평상시 반복 출현 규칙만. ★**판별 오류는 빈 목록으로 낮추지 않고 `unresolved`** |
| `location_part` producer | **§2-6.5 가 만든다.** 그것을 **중앙 획득의 선행조건**으로 올린다. 억지 승격 금지 |
| 실패 시 | ★**fail-closed 확정.** 「비차단」은 이것이 부가 기능이던 시절 계약이다. hard 예외를 살려 주 경로로 쓰는데 참조 없이 순수 T2I 로 내려가면 **예외의 의미가 사라진다.** provider/시간/예산 실패는 `retryable`/`time_capped`, 두 번 찾았는데 없으면 그 revision 의 `unresolved`. 사용자 override 만 별도 기록과 함께 진행 |
| 캐릭터 범위 | 다섯 owner **전부** assess 를 거친다 |
| 스텝 구성 | **두 단계** — 앞쪽(판별·보호·등록·질의 저장) + **중앙 획득 한 곳**(19.x) |
| 후보 수 | ★**기존 `MAX_CANDIDATES=4` 를 쓴다.** 사용자의 3~5 범위 안이라 **새 5 를 만들 이유가 없다** |
| 심판 수 | ★**기존 단일 심판 유지.** 새 다중 심판 **비용을 넣지 않는다** — coarse 계약이 **1심에서도** 돌아야 한다 |

### 완료 조건 (Codex)

**다섯 owner 모두 disposition 이 있고**, assess 가 hard 로 본 대상은
**stable row 또는 명시적 `unresolved` 중 하나**이며,
**`deferred` 가 소리 없이 사라지는 것이 0건**이다.

## 8. ★지울 것 · 이식할 것 (커밋 전 리뷰 대상)

### 지운다

| 무엇 | 어디 | 왜 |
|---|---|---|
| `generation_difficulty` enum·schema·지문·planner 소비 | 팩 `15.202608302200` · `grounding_classifier` · `grounding_planner` | `assess_subjects` 와 **같은 유료 판단**. 두 번 사면 안 된다 |
| `needs_reference_acquisition` · `reference_acquisition_ids` | `grounding_planner` | 위와 한 몸 |
| sourced-delta 를 **검색 여부 gate** 로 쓰는 경로 | §4b 배선 | 검색 여부는 assess 가 정한다 |
| 시대·국적·평범함을 묻는 old picker | 이 경로에서만 | 사용자 확정과 어긋난다. 야외 legacy 는 그대로 |
| 0~100 평균 최고점 선택 | 이 경로에서만 | 심판 눈금이 갈린다(실측 100 대 10) |
| 뒤쪽 **네 생산 호출** | `reference_image_generator:255` · `still_recipe_service:2159/4152/4477` | 중앙으로 옮긴다. 조회만 남긴다 |
| 내 `reference_acquisition` **스텝 13.8** | `step_manifest` · `steps/__init__` | 자리가 19.x 로 바뀐다 |

### 살린다 · **재배선한다** (★내가 「그대로」라고 쓴 넷이 전부 틀렸다 — Codex BLOCK)

| 무엇 | 실제로 손볼 것 |
|---|---|
| `coarse_type_pick` 팩·combine | ✅ 완료(`ae837c13`) — `PER_ROUND_CAP` 을 `era_research.MAX_CANDIDATES`(4)**에서 유도** · 1심 동작 잠금 |
| 라운드 계약 | 그대로. 2차 picker 는 **새 후보만** |
| **`era_research`** | ★**「안 건드린다」가 틀렸다.** `research_reference` 가 바로 옛 `pick_system` 과 `build_pick_user_head` 의 `REGION AND ERA` 를 부르고, **재검색이 없다**. coarse picker 와 1회 retry 를 쓰려면 **반드시 재배선**된다. `assess_and_research_cached` 도 판별과 획득이 **한 함수**라 앞쪽 판별/19.x 획득으로 나누려면 **경계를 분리**해야 한다.<br>★**불변인 것은 함수 몸체가 아니라 의미와 캐시 불변식**이다: `gemini-flash` · negative cache(빈 목록도 캐시) · failure non-cache · `failed_memo` · canonical-identity fail-safe · content-addressed bytes |
| **`materialize_missing_entities`** | ★**「그대로」가 틀렸다.** 지금 `grounding_planner.needs_reference_acquisition` 을 직접 import 해 `generation_difficulty` 에 매여 있다 — **그 축을 지우면 호출부가 0** 이 된다.<br>→ **era assess 가 저장한 hard obligation ID 를 명시 입력으로 받게** 바꾸고, **`prop`·`character`·`location` base 만** materialize 한다. `location_part`·outlook facet 은 **6.5a producer** 로 넘긴다.<br>★이름 포함 검사는 **완전성 보조**일 뿐 semantic binding 으로 **승격하지 않는다** |
| **`acquisition_contract_sha` · `acquisition_owner`** | ★**「그대로」가 틀렸다.** 지금 owner 이름·주석·상수가 **13.8 front** 를 가리키고 **legacy outdoor fallback 을 연다**. **중앙 19.x owner** 로 바꾸고, 이관 완료 뒤 late fallback 은 **제거**한다.<br>★그리고 outer invalidation 이 **assess 계약까지** 접어야 한다 — assess 팩·내용, assess 모델·physical model, policy, 그리고 acquisition(coarse·search·round). **screening 계약이 바뀌면 대상과 질의가 바뀌므로** `image_steps` 가 옛 참조를 봉인하면 안 된다.<br>screen identity 와 asset identity 를 따로 저장해도 되지만 **outer reference-pipeline sha 는 둘 다 접는다** |

### ★8.5 실행 순서 (Codex 확정) — **삭제는 맨 뒤다**

★BLOCK 4: 「old 축을 먼저 삭제하면 **replacement 가 아직 없다**」. 내가 낸
「지우기부터」 순서는 기각됐다.

| | 무엇 | 상태 |
|---|---|---|
| 1 | cap 4 + coarse 1심 보강 | ✅ `ae837c13` |
| 2 | `era_research` 를 **「cached assess plan」과 「cached acquire from plan」으로 분리** + 새 coarse/retry 계약 배선. ★**기존 late caller 동작은 유지** | ⬜ |
| 3 | 앞쪽 **screen obligation CP + base materialization** 을 실제 `entity_filter` 전 production path 에 배선. **다섯 owner 전부 disposition 기록** | ⬜ |
| 4 | **6.5a** producer/binding/gate + outlook obligation 결속 | ◐ **LP 몫 완료**(`19730ca2`) · outlook obligation 결속은 ⬜ |
| 5 | **중앙 19.x acquisition** 및 durable SOT | ⬜ |
| 6 | late 네 호출을 **중앙 조회로** 전환 · **끝점에서 실제 ref/role/sha 가 소비되는지 확인** · 21.915 read-only adapter | ⬜ |
| 7 | ★**그 뒤에만** `generation_difficulty` · 13.8 중복 스텝 · sourced-delta eligibility · late producer 를 **제거**. 계약 버전은 필드를 지워도 **감소시키지 말고 단조 증가**, 지문은 **새 팩** | ⬜ |
| 8 | outdoor seed 를 **중앙 CP 로 직접 겨눈 뒤** adapter 제거 | ⬜ |

### ★§2-6.5 를 **6.5a / 6.5b** 로 나눈다 (Codex)

`location_part` producer 물음의 답은 **(나)** 다. 다만 「producer 만 덜렁
먼저」가 아니라 **명시 분할**이다:

- **6.5a — 지금, 중앙 획득의 prerequisite**: `location_part`·outlook facet 의
  schema · **stable ID** · 원문 lineage · owner binding · disposition ledger ·
  **누락/중복/잘못된 base 승격 0** 을 보는 **기계적 gate**
- **6.5b — 뒤에 유지**: 사람/시각 acceptance 와 후속 품질 gate

★계획 표에 **전체 6.5 를 완료로 옮기지 않는다.**
「**6.5a producer prerequisite 선행 구현, 6.5b 미완**」으로 적는다.
★**6.5a 없이 중앙 획득도, 다섯 갈래 완료 주장도 금지.**

### 추가 정정 (Codex)

- 「old picker 제거, outdoor legacy 그대로」는 **공유 지문 자체를 지운다는 뜻이
  아니다** — `era_research` **경로를 coarse picker 로 재배선**한다는 뜻이다.
  legacy 가 쓰는 옛 팩은 **소비자가 없어질 때까지 보존**한다
- sourced-claims/§4b **전체 기록을 지우는 것이 아니다** — **reference
  acquisition eligibility 역할만** 제거한다. 기존 durable/audit 기록은
  역사와 다른 소비자를 위해 **보존**한다
- 중앙 획득의 owner 별 cache role 은 `prop` · `character` ·
  `location`(base/interior/exterior) · `outlook` · `location_part` ·
  `outlook_facet` 까지 **controlled ID** 여야 한다. **stable subject ID +
  role + canonical payload sha** 셋 중 **하나라도 없으면 합치지 않는다**
- `character` owner **도 assess/acquire 를 거친다.** 다만 얼굴·체형 identity
  SOT 를 **침범하지 않고**, 그 owner 에서 `era_research` 가 **실제 hard
  subject 를 반환했을 때만** 별도 reference obligation 이다.
  ★**제복을 글자로 character 에서 outlook 으로 옮기는 추측은 금지**

### 뒤쪽 처리 세부 (Codex)

1. `image_steps` 의 contract sha — **제거 아님.** 중앙 acquisition sha 를 접게
2. `outdoor_structure_form_reference` 21.915 — **당장 안 지운다.** 중앙 ref 를
   기존 CP 모양으로 내는 **read-only adapter** 로 먼저
3. outdoor seed 의 fail-closed 소비자를 **중앙 CP 로 직접 재배선한 뒤에만**
   adapter 를 없앤다
4. ★**late sidecar 가 캐시 SOT 가 되면 안 된다.** 중앙 durable CP/record 가
   SOT 이고 뒤쪽은 **읽기만** 한다
5. `location` 은 base/interior/exterior role 을 **controlled role** 로 분리.
   cache identity 는 모든 owner 에 대해 **stable subject id + controlled role +
   canonical payload sha** 를 요구하고, 셋 중 하나라도 없으면 **잘못 합치지 않는다**

## 9. 이 문서가 **말하지 않는 것**

- 「이관 완료」 — **아니다.** 뒤쪽 네 자리가 그대로 있다
- 「다섯 갈래 전부 된다」 — **아니다.** 지금 셋이고, `location_part` 는
  producer 자체가 없다
- §3c 통과 — **아니다.** 미확정 9/9 로 닫혔다
