# #77 — 완주 상태를 뚫는 방문 시점(JIT) 지문 검증 (2026-08-09)

> 구현 시점: **금월도 재생성 완주 후** (완주 전 코드 수정·재기동·유료 호출 금지).
> 방향은 사용자 지시 + Codex 교차 검증(08-09 12:30)으로 확정됨. 이 문서는 구현 상세와 결정점.

## 1. 문제 — 완료 판정이 지문보다 앞에 있어, 입력이 낡아도 안 잡힌다

같은 뿌리의 구멍이 두 층에 있다.

| 층 | 위치 | 구멍 | 실제 사고 |
|---|---|---|---|
| 스텝 | `image_steps.py:636` `"still_recipe_pack": "1.202607132300"` 하드코딩, `:641` judge 팩은 **버전 문자열만** | 팩 내용만 바뀌면 config_hash 가 안 움직여 **whole-step clean skip** | 팩 v7 을 올리고도 스텝이 재진입 안 함 |
| 샷 | `still_recipe_service.py:1605-1609` — `already_done_stills` 면 지문 비교 전에 `continue` | done 판정(`scene_persistence_service.py:382` = primary 존재 or CP 등록)이 **지문 무관** | 8/7 밤 11시간 재실행이 최종 스틸 311 중 253 을 지문 대조 없이 재사용 · lap1 prev 오염 샷 2개(S95sh18·S96sh1, transitive 후보 총 6)가 동결 |

샷 지문 재료 자체는 이미 온전하다: prev sel bytes 는 labeled_refs 로, 판정 팩은
`judge_pack_content`(bytes 해시, 커밋 `3dd8acc6`) 로 접혀 있다. **대조에 도달만 하면 잡힌다.**

## 2. 확정된 사실 (Codex 기록 전수 대조, 08-09)

- lap1 실패 95샷 = branch 429 **91** + branch SAFETY 1 + pre-branch fail-closed 3. **bg 단계 실패 0** → "옛 sel 을 낡은 앵커로 무경고 사용" 사례는 이번 실행엔 없음.
- "branch 실패 = sel 삭제" 는 항상이 아니다 — **fix 단계 실패면 새 `_sel` 이 남는다**.
- prev 직결 후속 52샷 중 50은 같이 실패(2바퀴가 스토리 순서로 앞을 먼저 만들므로 자연 치유). 완료·동결된 직접 위험 = **S95sh18·S96sh1**, transitive 후보 포함 **총 6샷**(정확한 목록은 구현 시 lap1 실패 로그 + prev 체인으로 재계산).
- **scan 시 일괄 지문 대조는 기각** — 스캔은 실행 전 한 번이라 그 시점엔 앞 샷이 아직 옛 bytes. 같은 바퀴에 수렴하지 않는다.

## 3. 설계

### 3-B. 샷 층 — already_done 완전 skip 을 "검증 재진입"으로 (핵심)

걷기는 스토리 순서(`:908`)이고 prev 체인은 선형이라, **방문 시점에는 앞 샷이 이미
이번 바퀴의 확정 bytes 다.** 그 자리에서 지문을 재계산하면 한 바퀴에 수렴한다.

`:1605` 게이트를 다음으로 교체:

```
if still_id in already_done_stills:
    if records.data.get(tag) is None:      # 주 record 전무(구세대 산출·손상)
        skip_grandfathered += 1            # 오늘과 같은 skip + 경고 카운터
        continue
    # record 있음 → 몸통 진입. 기존 단계별 지문 게이트가 그대로 판정한다.
```

- **지문 조립기를 따로 만들지 않는다.** 몸통의 기존 재사용 기제(`{tag}::bgfirst_bg`
  `:995`, bgfirst_full `:1213`, variants `:2489` cached_entry, conti_ab `:3007`
  prior_decision, 본 파이프 `multiroll_select.py:841` input_fingerprint)가 그대로
  판정한다. 병렬 조립기는 구현과 표류해 거짓 신선/거짓 낡음을 만든다(fake 를
  구현과 같은 함수로 만들지 말라는 규칙과 같은 뿌리 — 값은 같은 코드에서 나와야 한다).
- prev 해석(`:1752-1784`)도 몸통 것을 그대로 쓴다 — bg_only·공유 계획 분기까지 동일.
- **전 단계 일치 시 부작용 0**: 아무 단계도 재생성하지 않았고(지출 0) 최종 sel 이
  기존 primary 와 같으면(bytes 비교, 로컬) persist·primary 재설정 없이 continue.
  하나라도 재생성되면 기존 경로대로 persist + primary 갱신.
- **연쇄 수렴**: 오염 샷이 재생성되면 새 sel bytes 가 다음 샷의 prev 재료가 되고,
  다음 샷도 방문 시 mismatch 로 잡힌다. 걷는 순서가 곧 의존 순서라 별도 closure
  계산이 필요 없다.
- record 전무 grandfather 는 **records.json 파싱 실패(`_Records` → `data={}`)가
  전량 재지출로 번지는 것도 막는다** — 그 경우 오늘과 같은 동작으로 내려앉고
  경고·카운터로 드러난다(조용한 절단 금지: 카운터는 로그 + 모니터에 노출).

### 3-A. 스텝 층 — config_hash 에 팩 '내용' 해시

`image_steps.py` payload 수정:
- `:636` 하드코딩 `"1.202607132300"` → 실사용 selector 해석값 + **팩 디렉토리 bytes 내용 해시**
- `:641` `multiroll_judge_pack` 버전 문자열 옆에 **내용 해시 병기**
  (`judge_pack_content_hash` 를 팩 일반형으로 승격해 재사용 — multiroll_gemini 의 것과 한 구현)

**구현에서 확인된 사실(2026-08-09)**: config_hash 가 움직인 완료 스텝의
resume 은 기본 **BLOCK** 이다(`step_runner.py:769` — 사용자 의도 확인 정책,
force 요구). force 는 금지이므로 승인 레버를 만들었다 —
`STEP_CONFIG_DRIFT_ACK`(쉼표 구분 step_id) 에 스텝을 명시하면
`image_steps._evaluate_contract_drift` 재정의가 RERUN_SELF(origin=
`config_drift_ack`) 를 돌려주고, 이 origin 은 runner 의 어느 force 격상
갈래에도 안 걸려 **cleanup/invalidate/clear 없이 resume 모드로 실행**된다
(`:1160` fall-through → `_execute_rerun_self`). 실행 서비스도 BLOCK 외
판정은 그대로 백그라운드에 넘긴다(`step_execution_service.py:179`).
걷고 나면 새 hash 로 CP 가 다시 서므로 승인은 사실상 1회용.

### A+B 는 한 몸이다

A 만 있으면: 스텝은 재진입해도 scan 이 전 샷을 done 으로 걸러 253-재사용이 재발.
B 만 있으면: 팩 내용만 바뀐 경우 스텝이 clean skip 이라 몸통까지 못 온다.
**같이 넣어야 "팩 올림 → 스텝 재진입 → 전 샷 0원 검증 걷기 → 낡은 샷만 재생성" 이 완성된다.**

### Codex 리뷰 2·3라운드 반영 (2026-08-09 저녁 — BLOCK 5건 전부 수용)

1. **봉인 금지**: 검증 샷의 재생성 실패는 옛 primary 가 남아 스텝 집계에
   안 잡힌다 → 걷기 끝에 `StillJitVerifyIncomplete` 로 스텝을 실패로
   남긴다(새 config_hash CP 가 completed 로 봉인되는 것 차단).
2. **지출 판정 ≠ bytes**: bytes 비교는 "새 asset 영속 필요 여부"만.
   지출은 **record 묶음 변화**로 판정(`_jit_tag_snapshot` 전후 대조) —
   유료 재개 경로(소급 critique 등)는 전부 record 를 갱신하는 재개 규율이
   근거. 표기용 키(ref_mode 등)·바퀴마다 뒤집히는 표식(reused·*_this_run)
   은 걸러 거짓 지출 차단. bytes 동일+record 갱신 = 영속 생략하되 상한
   계수 포함(jit_spent_same).
3. **공유 record 포함**: `groupbg::장소` 처럼 tag 밖 공유 record 의
   갱신도 그 샷의 지출로 귀속(걷기가 샷 단위 직렬) — `::` 접두가 샷 tag
   가 아닌 키 전부를 스냅샷에 포함(새 공유 네임스페이스 자동).
4. **상한 래치**: 상한 도달(다음 검증 대상 앞 + 정확히 상한으로 걷기가
   끝나는 경계 포함) 시 `recipe/JIT_REGEN_LATCH.json` 영속 후 raise.
   래치가 있으면 `SceneImagePipelineStep._execute` **첫 문장**에서 mode
   무관 즉시 실패 — 선행 유료 구간(world guide 캐시 미스·T2I fallback)과
   force 의 recipe 아카이브(래치까지 치우는 우회)보다 앞. 자동 재시도는
   지출 0 으로 끝난다. 해제 = 사람이 원인 확인 후 파일 삭제.
5. 후속 과제(범위 밖 기록): 유료 adapter 호출 카운터 seam — record 규율
   밖의 무기록 유료 호출은 별도 결함 부류(재개마다 재지출)라 따로 잡는다.

## 4. 비용·안전 가드

1. **0원 검증 성질의 증명**: 몸통 안 유료 호출 지점 전수 감사 — 각각이 record
   게이트 뒤인지 확인 + "record 전부 일치면 유료 호출 0회" 유닛(실제 생성 함수
   이음새에 호출 카운터, 지문 로직은 프로덕션 코드 그대로).
2. **재생성 폭주 상한**: JIT mismatch 재생성 수가 상한(제안: 64)을 넘으면 그
   자리에서 BLOCK + 진단 로그(어느 지문 키가 움직였는지 분포). 지문 체계가 바뀐
   직후 전량 mismatch 같은 사고를 확인 없이 과금으로 옮기지 않는다.
3. **되돌림 레버**: env 1개(기본 on). off = 오늘의 완전 skip. 새 플래그를 늘리는
   것이라 env 전수 조사 과제에 등록.
4. config_hash 변경(A)은 전 프로젝트 1회 재진입을 낳는다 — B 가 함께 있으면 그
   재진입은 0원 검증 걷기다. 완주 전엔 절대 넣지 않는다(RERUN_SELF 동등성 파괴).

## 5. 구현·검증 순서 (완주 알림 후) — 2026-08-09 실행판

1. 완주 확인(개수·최신 mtime·감시자) ✓ 18:09 완주, 256/256 실패 0.
2. A+B 구현 + 유닛 ✓ — 신규 22 통과, unit+services+core 4,754 통과
   (실패 15는 변경 전에도 동일 실패 — stash 대조로 확인한 기존 부채).
3. Codex 리뷰(구현 diff) → 심각(돈·결과 오독·기록 깨짐)만 수용.
4. 마지막 한 바퀴 절차:
   ① `.env` 에 `STEP_CONFIG_DRIFT_ACK=scene_image_pipeline` 추가
   ② 백엔드 재기동 + **기동 시각 확인**(ps — 돌던 프로세스가 옛 코드면
      전부 헛일이 된다)
   ③ 로그인 후 `POST /steps/scene_image_pipeline?mode=resume` 1회 —
      config drift → 승인 갈래 → 비파괴 재실행(force 금지, drive 루프
      불필요 — 실패 시에만 `drive_step.sh` 재사용)
   ④ 기대: "JIT 검증" 로그로 대부분 그대로 통과(지출 0), 재생성은
      S95sh18·S96sh1 + transitive ≤4 부근만. 상한(64) 도달 시 BLOCK.
   ⑤ 완료 후 `.env` 에서 ack 제거(새 CP=새 hash 라 이후 resume 은 SKIP).
5. 확인: 재생성 목록 vs 예측, 요약 카운터 로그, Opik 호출(재생성 샷만),
   신규 primary 수.
6. 커밋 분리: (A+B 코드+시험) / (문서·메모리). 기존 하네스 플래그 수정
   (`test_still_recipe_target_scope.py` — 08-05 기본값 승격이 깨뜨린 것)
   은 코드 커밋에 포함.

## 6. 확정된 결정 (2026-08-09 사용자 합의 — 셋 다 제안대로)

1. 재생성 상한 기본값 = **64** (256샷 에피소드의 25%, env 로 조정 가능).
2. record 전무 done 샷 = **skip 유지 + 경고 카운터**. 강제 재검은 이번 범위 밖(필요해지면 별도 env 로).
3. 상한 초과 = **BLOCK(멈춤)**. 돈 사고 방지가 우선이며, autodrive 가 BLOCK 을 완주로 오독하지 않게 종료 사유를 명시 로그로 남긴다.
