# era 참조 캐시가 「같은 장소」를 못 알아본다 (감사 2-C)

## 실측 — 적중 1.1%

records 전수 19파일. 저작된 대상 **179건 중 서로 다른 문자열 177종**.

가장 작은 판(`da049582`, 3씬 「골목 끝」, 물리 장소 **2곳**)에서 조사가
**6번** 돌았다. 자전거 수리점 하나가 네 번이다:

    한국의 동네 자전거 수리점 (자전거포) 내부 작업 공간
    대한민국의 동네 자전거 수리점 내부 (2020년대)
    현대 한국의 동네 자전거 수리점 내부 작업 공간
    대한민국의 현대식 동네 자전거 수리점 내부

골목 사거리는 두 번. 큰 판도 같다 — `5bddbdfc` 63키/73대상,
`fdc5f9f0` 62키/69대상으로 **전부 1x**.

뿌리는 `era_research.py` 의 캐시 신원이 **LLM 자유 저작 세 칸**
(`subject_native`·`search_terms_native`·`language_lock_native`)에 매여
있던 것이다. 조금 다르게 부르면 새 키가 되고, 새 검색·다운로드·심판을
산다.

## 기각한 안 — 모델에게 종류를 묻기

처음엔 `target_kind` 를 판별 스키마에 새 칸으로 받고 코드가 열거값을
검사하려 했다. **Codex 가 기각했고 그 판단이 맞다**: enum 검사는 문자열
범위만 볼 뿐 **뜻이 맞는지는 여전히 자가신고**다. 그 값을 캐시 신원으로
믿으면 캐시 미스보다 나쁜 **다른 장소 참조 재사용**이 난다.

복합 대상 분해도 뺐다 — 캐시 소비자는 `subjects[0]` **하나만** 조사하므로
(era_research.py:426) 스키마에 분해를 넣어도 두 번째는 아무도 안 쓴다.
다중 참조 배선은 새 기능이라 이 판의 범위가 아니다.

## 정본 신원 세 칸

    canonical_scope_id    scene_director.primary_location
    canonical_scope_role  location_interior | location_exterior
    canonical_scope_sha   그 id 의 정본 내용(이름+서술) 해시

★`primary_location` 은 **UUID 가 아니라 `L01` 같은 short_id** 다(실물
확인). 그리고 `background_classify` 의 `members[].loc_id` 와 **같은
공간**이라 변환이 필요 없다 — UUID 로 굳이 바꾸면 변환 한 단이 늘고 그
변환이 틀릴 자리가 생긴다.

★**역할은 코드가 정한다.** `background_classify` 의 `is_indoor` 에서
읽는다. 같은 `L01` 이라도 안과 밖은 다른 참조라, 한 값으로 두면 실내
뷰와 실외 뷰가 한 장으로 합쳐진다.

★**입력도 같이 맞춘다.** 키에서만 빼면 「첫 샷의 문안이 그 장소 전체의
결과가 되는」 순서 의존 캐시가 된다. 판별에 보내는 것도 같은 정본이다.

★**셋 중 하나라도 없거나 어긋나면 합치지 않는다** — 옛 자유 키로
떨어지고 `identity_fallback` 과 사유를 기록에 남긴다. 잘못 합치는 것이
중복 조사보다 나쁘다.

## (A) vs (B) — 실측이 결정했다

`still_recipe_service` 는 `background_classify` 를 **안 읽었다**. 그래서
새 CP 의존을 선언할지가 갈렸다. 이미 손에 쥔 `outdoor_place_spec` 만
쓰는 (B) 안을 55판 전수로 재 봤다:

    55판 중 **29판이 덮개 0%**  (그 CP 가 아예 안 돈 판)
    도는 판도 36~71%
    ★덮는 것은 정확히 `is_indoor:false` — **실외만**

큰 판은 loc 의 절반 이상이 실내다(`c7e3b2e7` 93개 중 58개 ·
`fdc5f9f0` 69개 중 44개). **조사가 몰리는 곳이 바로 그 실내**다 —
국밥집·경찰서 사무실·자전거 수리점. (B) 는 문제의 큰 쪽을 통째로 안
건드린다. `background_classify` 는 모든 판의 모든 loc 에 `is_indoor` 를
갖고 있다(결손 0).

그래서 **(A)** — 읽고, `depends_on` 에 **선언**한다. 안 선언하고 읽으면
숨은 의존이 되고 그건 이 저장소에서 이미 결함으로 적힌 모양이다.

## 지문 — `depends_on` 은 안 움직인다

★`compute_config_hash` 는 `project_config` 만 접는다(`step_runner.py:112`).
**`depends_on` 을 넣어도 지문은 안 움직인다** — 처음에 반대로 알고
있었다. 움직이는 것은 `ERA_RESEARCH_POLICY_VERSION` 이고, 그 문자열이
스텝 지문(`image_steps.py:829`)과 샷 지문
(`still_recipe_service.py:1164`) 둘 다에 접힌다. 신원 계약이 바뀌었으니
**v2 → v3** 로 올렸다. 안 올리면 완주한 판이 옛 신원으로 만든 참조를
새 계약인 것처럼 재사용한다.

    era_research_v2_plate_confined
    → era_research_v3_canonical_scope_plate_confined

적용 범위 낱말(plate·confined)은 **이름에 남겼다** — v2 가 그것을
담으려고 올린 판이고 시험이 그 두 낱말을 잠근다.

## 되살아날 뻔한 것

fallback 을 `subject_text` 로 만들었더니 기존 시험이 잡았다 —
「샷마다 다른 문장이 같은 대상을 내면 조사는 1회」라는 이득이 깨졌다.
fallback 은 **옛 조립 그대로**(저작된 대상·질의·잠금문) 둔다. 이 판이
고치려는 것은 **저작이 흔들리는 것**이지 저작이 같을 때의 병합이 아니다.

## 시험이 잠그는 것

    같은 장소의 여러 문안 → 판별 1회·조사 1회
    같은 L01 의 안/밖    → 갈린다
    L01 vs L02           → 절대 안 합쳐진다 (거짓 적중 양성 대조)
    정본 내용이 바뀌면    → 키가 움직인다
    신원이 불완전하면     → 안 합치고 사유를 남긴다
    스키마 maxItems      → 1 (소비 계약과 일치)

옛 신원으로 되돌려 세 시험이 빨강이 되는 것을 확인했다.

---

# canary (2026-08-27) — 나가는 검색어가 한 대상인가

★**이것은 모델 행동 확인이다** — 캐시 병합 계약과 **분리해서** 읽는다
(Codex). 병합·분리는 단위 시험이 잡는다. 여기서 보는 것은 「정본 문안 +
`maxItems=1` 로 저작된 검색어가 **사진 한 장에 담기는 대상 하나**를
말하는가」뿐이다.

유료 assessor 3콜(L01 · L03 · L03 재실행 — 첫 판 출력을 내 필터가
잘라먹어 다시 받았다), **이미지 0장**. 입력은 EntityCanon 의 이름+서술
그대로 — 이 판에서 판별에 실제로 보내는 것이다.

## L03 사거리 — 역사적으로 복합이 나던 자리

옛 저작(실측 records, 같은 장소):

    한국의 현대 골목길 사거리
      ['골목길 사거리', '동네 골목길 전봇대', '야간 골목길 가로등', '한국 주택가 골목']
    대한민국의 현대식 사거리 도로 노면 표시 **및** 횡단보도
      ['한국 도로 노면 표시', '도시 사거리 횡단보도', '야간 아스팔트 차선']

→ **두 번 조사**했고 하나는 대상이 둘이었다.

새 저작:

    2020년대 대한민국의 주택가 골목길 사거리
      ['한국 골목길 야경', '주택가 골목 사거리', '동네 골목길 밤']

→ **하나**. 검색어 셋이 같은 한 장소로 모인다. 노면 표시·횡단보도는
별도 대상이 되지 않고 `reason_ko` 로 내려갔다.

## L01 정비소

    2020년대 한국의 동네 자전거 수리점 내부
      ['동네 자전거 수리점', '자전거 정비소 내부', '자전거 수리점 작업대']

옛 저작은 같은 장소를 네 이름으로 불렀다(위 「실측」절). 여기선 하나다.

## 아직 안 한 것

`_run_groupbg` 를 db·CP·이미지·persistence 까지 세워 끝점에서 태우는
픽스처는 **세우지 않았다**(Codex 판단). 근거 셋: 기존
`test_resolve_groupbg_canonical_origin_contract` 가 follower 에서 origin
보존을 이미 직접 재고, era 모듈 시험이 캐시 동작을 재고, 새 AST 시험이
그 사이 **한 줄 배선**(`origin_tag`)을 잡는다. 이번 결함은 거대한 서비스
실행 의미가 아니라 변수 하나 선택이었다.

대신 **다음 실주행 첫 groupbg 방문**의 `scope_id` 가 origin 씬의 loc
인지 대조하는 것을 acceptance 로 둔다. 그 값은 이미 기록에 남는다.

★**어느 항목을 볼지 한정한다** (Codex 비차단). records 한 파일에는
 여러 scope 의 `era_assess::*` 가 같이 있어서, 아무거나 집으면 **다른
 장소의 결과를 origin 것으로 오독**한다. 둘 중 하나로 본다:

    ① 새 records (그 판의 첫 조사)
    ② 첫 groupbg 방문 **전후의 delta** — 그때 새로 생긴 항목만
