# C — 구간마다 한 번만 읽는다. ★아직 **결정안이 아니다**

Codex 가 유료 실험 전에 **무료로 닫으라**고 한 다섯 가지다 (2026-08-31).
여기 적힌 것 중 **모르는 것은 모른다고 적었다** — 숫자로 위장하지 않는다.

## 0. 왜 A·B 가 기각됐나

    entity_all   「**2회 이상** 나오면 등록」        ← 반복 출현 축
    A0/고증 예외 「**한 번만 나와도** 어려우면 등록」 ← 예외 축

이 둘은 **중복이 아니다**. 한 호출로 합칠 수는 있어도 **union 을 보존**해야
한다. A 는 반복 엔티티를, B 는 일회성 예외와 `location_part`·`outlook` 을
잃는다.

## ① C 의 schema 와 **기존 칸의 행선지**

한 구간의 산출은 행 목록이고, 행마다:

| 칸 | 어디서 오던 것 | 소비처 |
|---|---|---|
| `chunk_id` · `row_index` | (새것) | reducer 가 신원을 만든다 |
| `owner_type` (5 enum) | A0 `owner_type` | 갈래 배정 |
| `surface_form` | A0 · `entity_all.name` | 표시·audit **뿐** |
| `source_quote` · `source_anchor` | A0 | 근거 · 사람 확인 |
| `appears_here` (bool) | `entity_all.shot_count` 의 구간판 | reducer 가 **합산** |
| `hard_to_generate` (bool) | classifier `generation_difficulty` | 예외 축 |
| `viewers_would_notice` (bool) | assess 의 두 조건 중 하나 | 예외 축 |
| `visual_brief` | `entity_extract.description` | 하류 T2I |
| `search_terms_native` · `language_lock_native` | assess | 참조 획득 |
| `existing_id \| new` | (새것 — ③) | reducer 가 결속 |

### ★①은 닫혔다 — classifier 고유 칸은 **`route` 를 만드는 데만** 쓰인다

`grounding_class` · `referent_specificity` · `visibility_intent` ·
`visible_discriminators` · `locale` · `generation` 여섯 칸을 코드에서 찾으면
**classifier·planner 밖 언급이 0건**이다. (`confidence` 는 87건이지만 전부
다른 계통의 제 칸이다 — pose judge · evidence helper 등.)

그리고 `route` 를 읽는 곳은 **셋**뿐이다:

| 어디 | 무엇에 |
|---|---|
| `grounding_overlay.protected_short_ids` | 저빈도 필터 **보호** (`research`·`design`·`unresolved`) |
| `grounding_steps:797` | `grounding_research` **대상 고르기** (`route == "research"`) |
| `grounding_steps:336,394` | 세는 것뿐 (telemetry) |

C 가 `appears_here`(출현) · `hard_to_generate` · `viewers_would_notice` ·
참조 의무를 직접 내면 —

    보호      = `출현 합계 ≥ 2 OR (어려움 AND 알아챔)`  ← 사용자 규칙 그대로
    조사 대상 = 참조 의무가 선 행                        ← C 가 직접 낸다

둘 다 `route` 없이 선다. **그러므로 classifier 6콜은 지울 수 있다.**

★그 밖의 칸도 따라가 봤다 — `interpretation_kind`·`trait`·
`attribution_note` 는 **grounding 계통이 아니다**. `entity_extractor_v2` 팩
(`turn1_7_detail_batch_schema.json`)의 **외형 특질** 칸이고, `entity_steps` 가
「원문 인용으로 확인된 문자적 특이 외형」을 짤 때 쓴다. C 와 무관하다.
`unstable` 은 telemetry 한 줄뿐이다.

**→ ① 닫힘. classifier 를 지우는 데 걸리는 칸은 없다.**

### ★C(c) **전용** schema (초안) — 한 구간의 산출

★Codex 가 **(c) 를 잠정 목표**로 골랐다 (2026-08-31): 원문 각 구간을
**병렬로 한 번만** 읽고, 뒤 merge 는 원문을 다시 해석하지 않고 compact row
의 **동일성만** 판단한다. 벽시계 우선 조건에 (b)(순차)보다 맞는다.
**(b) 는 fallback 으로만 남기고 두 구현을 같이 만들지 않는다.**

그래서 이 schema 는 **(c) 전용**이다 — `existing_id | new` 는 **없다**.

```jsonc
{
  "rows": [{
    // ── 신원 ── ★모델이 안 만든다. 코드가 chunk_id#row_index 로 발급하고
    //            여기서는 row 순서만 받는다.
    "owner_type": "prop|character|location|location_part|outlook",
    "surface_form": "string",          // 표시·audit **뿐**. 결속 근거 아님

    // ── 출현 ── ★bool 이 아니다. 한 구간 안 2회를 1로 세면 규칙이 깨진다
    "occurrences": [{ "source_anchor": "string", "source_quote": "string" }],

    // ── 두 축 ── ★코드가 `≥2 OR (hard AND notice)` 로 합친다
    "hard_to_generate": true,          // 이미지 모델이 형태를 틀릴 만한가
    "viewers_would_notice": true,      // 그곳 사람이 알아채는가

    // ── 하류가 쓰는 것 ──
    "visual_brief": "string",          // entity_extract.description 자리
    "search_terms_native": ["string"], // 참조 의무가 설 때만 채운다
    "language_lock_native": "string"
  }]
}
```

**안 넣는 칸**과 그 이유:

| 안 넣음 | 왜 |
|---|---|
| `stable_id` | 모델이 쓰면 매번 달라진다. **코드가** 발급한다 |
| `grounding_class` · `referent_specificity` · `visibility_intent` · `locale` · `generation` · `visible_discriminators` | `route` 를 만드는 데만 쓰였고, 위 두 축이 `route` 를 대신한다(①) |
| `generation_difficulty` 3값 enum | `hard_to_generate` bool 로 접는다. 3값이 필요한 소비처가 **없다** |
| `planned_occurrences` | 지금도 **읽는 곳이 0** 이다 |
| `existing_id_or_new` | **(b) 순차의 칸**이다. (c) 는 코드가 발급하고 merge 가 동일성을 판단한다 |

## ② 구간 경계 — ★**이미 있는 관례를 쓴다**

새 수를 지어내지 않는다. 이 저장소에는 「모델이 원문을 한 번에 얼마나
읽는가」에 대한 답이 **이미 있다** —

    entity_lister.BUNDLE_TARGET = 3000 (자)   ← 씬 경계에 맞춰 묶는다
    beat_shot_steps.BUNDLE_TARGET = 3000       ← 같은 값

그 규칙 그대로 묶으면 (`segment_bound.py` 가 같은 코드로 잰다):

| 원고 | 씬 | **구간 S** | 구간당 하한(최대/평균) | 합계(중복 포함) |
|---|---|---|---|---|
| 66천자 | 113 | **26** | 14 / 8.8 | 230 |
| 41천자 | 94 | **16** | 25 / 18.2 | 291 |
| 9.6천자 | 30 | **4** | 24 / 18.2 | 73 |

★**호출 수는 오늘보다 늘어난다** — 26·16 대 오늘 알려진 14. 작은 원고만 준다.
그러니 「C 는 호출을 줄인다」는 **틀린 말**이다. C 의 이득은 다른 데 있다(⑤).

## ③ ★★구간을 넘는 신원 — **C 의 빈 자리**

같은 실물이 두 구간에 나오면 `source_anchor` 가 달라 **다른 ID 가 된다.**
그것을 표면형이 같다고 코드가 묶으면 **금지한 글자 판단으로 돌아간다.**
「같은 행이라 결속이 필요 없다」는 **한 구간 안에서만** 참이다.

★그리고 내가 크기를 잴 때 쓴 `scene_director.present_entity_ids` 는
`C01`·`L01` 같은 **`short_id`** 이고, 그것을 발급하는 것은 `entity_all` →
`entity_merge` 다 — **C 가 대체하려는 바로 그 추출의 산출**이다. 순환이라
C 의 신원 근거로 못 쓴다.

해법은 둘뿐이다 (Codex):

**(a) C 보다 앞에 권위 있는 구조 ID 가 이미 있으면** 그것을 입력 슬롯으로
쓴다. → **없다.** 앞에 있는 것은 `scene_save`·`shot_validator` 뿐이고
거기에 실물 ID 는 없다.

### ★ID 안정성의 **범위** (Codex 정정)

「entity-stable ID」가 아니다. **검증된 정본 evidence set 이 같을 때만**
같은 ID 다. 모델이 같은 실물을 **다른 실제 span** 으로 고르면 ID 는 달라질
수 있다 — 그건 계약이 막는 것이 아니라 모델 판단이다.

막는 것은 셋뿐이다: 도착 순서 · 모델 행 순서 · 모델이 쓴 인용 문자열.

**(b) 순차 1-pass-per-chunk.** 구간을 **독립 병렬로 안 본다**.
코드가 지금까지 발급한 ID 로 **compact catalog** 를 만들어 다음 구간에 넘기고,
모델은 행마다 `existing_id | new` 를 **구조화로 고른다**. 새 ID 는
**코드가** 발급한다 — 모델이 안정 ID 를 써내는 안은 **안 된다**.

**(c) 병렬 구간 + `entity_merge` 가 구간 사이 중복을 지운다** ← ★내가 더한 안

(b) 의 대가는 **순차**이고, 그건 벽시계 시간이다 — 제약이 시간인데 그걸 늘린다.
그런데 「같은 실물이 두 번 등록됐다」를 지우는 **스텝이 이미 있다** —
`entity_merge` 다. 지금은 **갈래 사이** 중복을 지우는데, **구간 사이**도 같은
물음이다.

    구간 S개를 **병렬로** 돌린다 → 각 구간은 chunk-local 행을 낸다
                                    (ID 는 **코드가** 발급)
    `entity_merge` 가 그 행 전부를 보고 `keep`/`remove` 쌍을 낸다
    → 3b 가 이미 만든 계약 그대로 — provenance union · 못 옮기면 삭제 거부

★신원 판단은 **모델의 구조화 판정**이고 코드의 글자 대조가 아니다 —
사용자 계약 14 를 지킨다.

**크기가 되는지 실측했다** (행당 bytes × 구간 합계 행):

| 원고 | 오늘 merge 입력 | S=1 | S=3 | S=8 |
|---|---|---|---|---|
| 66천자 (행당 468 B) | 74,837 B | 21,528 | 41,652 | 63,648 |
| 41천자 (행당 331 B) | (미측정) | 46,009 | 63,552 | 78,116 |

대부분 오늘 아래지만 **41천자 S=8 의 78,116 B 는 오늘(74,837 B)보다 크다** —
「오늘 범위 안」이라고 뭉뚱그리면 안 된다(Codex). 그리고 이건 **크기 참고**일
뿐 품질 근거가 아니다.

### ★★(c) 는 「기존 merge 그대로」가 **아니다** (Codex 정정)

내가 「`entity_merge` 가 이미 그 일을 한다」고 쓴 것은 코드와 다르다 —

    지금  character/location/prop **3갈래**만 읽고
          「**타입이 다른데** 같은 대상」·「각 타입 **쌍**」만 찾는다

병렬 구간의 핵심은 **같은 owner 안의 중복**이고 C 는 **5갈래**다.
지금 계약으로는 **둘 다 못 본다.** 그래서 (c) 는 **확장 merge 설계 후보**이지
③이 닫힌 상태가 아니다.

그 확장 계약을 `grounding_chunk_merge.py` 에 적었다 (★**아직 아무 데도 안
붙인다**, 시험 29건):

1. **5 owner · same-owner cross-chunk** 를 받는다
2. 출현은 `bool` 이 아니라 **`occurrences[{source_anchor, source_quote}]`** —
   한 구간 안 2회를 1로 세면 「2회 이상」이 그 자리에서 깨진다. **고유 anchor**
   를 센다
3. `existing_id | new` 는 **순차 (b) 의 칸**이다. (c) 는 코드가
   `chunk_id#row_index` 로 발급하고 keeper 최종 ID 도 **결정적 규칙**
4. **모든 remove** 는 유효 keeper 하나가 없으면 삭제 거부 — 「근거 든 행만
   보호」로는 구간 행이 사라진다
5. `location_part` 는 `same_referent` 와 **`part_of` 를 구조로 가른다**.
   빠진 `relation` 을 짐작하지 않는다 — 짐작이 부분을 지운다

(c) 의 남은 위험:

- merge 가 **다른 두 실물을 잘못 합치면** 하나가 사라진다. 계약이 「못 옮기면
  거부」라 완전 소실은 막지만 **오합치 자체는 못 막는다** — 모델 판정이다
- 구간 합계 행이 S 에 따라 커진다 — merge 입력도 같이 큰다
- ★**아직 안 잰 것**: merge 가 그만큼을 한 번에 제대로 가르는지. 유료다

(b) 에 반드시 딸리는 계약:

- 한 catalog id 를 **두 행이** 고르면 → `contested`, 임의로 안 고른다
- 모델이 목록에 없는 id 를 쓰면 → 버리고 기록
- 「고를지 말지」를 못 정하면 → `unresolved`, **승격 안 한다**
- 순차라서 **구간을 병렬로 못 돌린다** — 시간이 늘어난다 (★맞바꿈)

## ④ 크기 — **하한만** 적는다

지금 알 수 있는 것은 `entity_extract` 세 갈래 산출의 **합계**뿐이다.

| 원고 | 세 갈래 상세 합계 | C 에 **더해지는 것** |
|---|---|---|
| 66천자 | 51,491 B | `location_part`·`outlook` 행 · 두 flag · 원어 질의 · 인용 |
| 41천자 | 54,015 B | 〃 |
| 9.6천자 | 25,052 B | 〃 |

### ★schema 초안으로 다시 잰 산출 (④)

위 schema 초안 한 행을 실제 JSON 으로 만들어 재면 —

    참조 의무가 **선** 행 (질의 칸이 찬 경우)  ≈ **1,312 B**
    안 선 행                                   ≈   **866 B**

구간 합계 행(하한)에 곱하면:

| 원고 | S | 합계 행(하한) | 산출 ≈ (다 찼을 때) | 구간당 평균 |
|---|---|---|---|---|
| 66천자 | 26 | 230 | 301,760 B | 11,606 B |
| 41천자 | 16 | 291 | 381,792 B | 23,862 B |
| 9.6천자 | 4 | 73 | 95,776 B | 23,944 B |

**구간당 산출은 12~24 KB** 로, 오늘 성공하고 있는 한 응답의 최대
(25,627 / 23,144 / 9,768 B)와 **같은 자리**다.
→ 잘림 위험이 지금보다 크다고 볼 근거는 **없다**. 다만 **행 수는 하한**이라
실제로는 더 많고, 「다 찼을 때」는 **모든 행이 참조 의무를 진 경우**라
실제보다 크다. **두 오차가 반대 방향**이라 어느 쪽으로 얼마나 틀리는지는
**모른다**.

총 행수·bytes 는 여전히 **미상**이다.

★앞 판에 적었던 「출력 3배 → 구간 3개」는 **틀린 셈**이었다 —
`ceil(3×B / B)` 라 언제나 3이 나오는 순환식이고, 엔티티가 구간에 **겹쳐
나타난다**는 것도 안 셌다. 실측하면 S=3 이 행 수를 1/3 로 안 줄이고
**총 산출은 오히려 커진다**.

## ⑤ 지금 대비 호출 그래프

    지금   A0 1 + entity_all 3 + entity_extract 3 + merge 1
           + classifier 6 + research R + (screen N — 꺼져 있음)
           = 알려진 14 + R

    C(b)   chunk 1콜 × S (**순차**) + merge 1
           + research R'(참조 의무가 선 대상만)
           = S + 1 + R'

①이 닫혔으므로 classifier 6 은 뺐다.

### ★C 의 이득은 **호출 수가 아니라 입력 총량**이다

오늘은 **원문이 4번, 샷 전부가 3번** 나간다. C 는 **원문이 한 번**만 나가고
구간마다 고정 몫(지문·schema·세계관)이 붙는다. 생산 관례 경계로 재면:

| 원고 | S | C 입력 **하한** | 오늘 | 비율 |
|---|---|---|---|---|
| 66천자 | 26 | 525,192 B | 1,836,632 B | **29%** |
| 41천자 | 16 | 276,962 B | 1,503,257 B | **18%** |
| 9.6천자 | 4 | 70,474 B | 468,113 B | **15%** |

★**하한**인 이유 — C 지문이 아직 없어서 A0 지문(4,670 B)+schema(628 B)로
대신 셌다. 실제 C 지문은 다섯 갈래와 여덟 칸을 설명해야 하니 **더 크다**.
그래도 원문을 3번 덜 보내는 몫(≈100~450 KB)이 훨씬 크다.

**그래서 정리하면** — 호출 수는 **늘고**(14→27·17·5), 입력 총량은 **1/3~1/7 로
준다**. 벽시계는 (c) 병렬이면 구간이 동시에 도니까 유리하지만 **안 쟀다**.

★③의 해법이 (b)(순차)냐 (c)(병렬+merge)냐로 **벽시계 시간이 갈린다**.
지금 `entity_all` 3 과 `entity_extract` 3 은 서로 **병렬로 돌 수 있다**.

    (b) 순차     호출 S+1 인데 **직렬** — 호출이 줄어도 시간은 안 줄 수 있다
    (c) 병렬     호출 S+1 이고 구간이 **동시에** 돈다 — merge 만 뒤에 선다

제약이 돈이 아니라 시간이므로 **(c) 가 유리하다.** 다만 (c) 는 merge 에
구간 사이 신원 판정을 맡기는 것이고, 그것이 제대로 되는지는 **아직 안 쟀다**.

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

- C 가 지금보다 낫다 — **아직 모른다**
- 구간 수 — **안 정했다**
- 품질 — 크기와 별개다. 재려면 유료다. **유료 실험은 구조가 확정된 뒤**에만.
