검색 그라운딩 씨드 production 승격 · 2026-07-28 ~ 07-30 · 막힌 지점 20건의 원인과 조치 (18건 해결 · 1건 미해결 · 1건 오탐) · 최종 결과 최종 샷 237/237 · form_reference 10/10 · seed 25/25
| # | 지점 | 귀속 | 결과 |
|---|---|---|---|
| 1 | text_cleanup 빈 응답 | 시스템 | 분할 재시도 추가 · 통과 |
| 2 | scene_detail 계약 위반 | 시스템 | 샷 단위 재저작 · 통과 |
| 3 | redo checkpoint_sync | 시스템 | catalog import · 통과 |
| 4 | is_primary 타입 | 내 버그 | 수정 · 스모크 검증 |
| 5 | 그룹 격리 실패 | 내 버그 | rollback 추가 |
| 6 | created_at 누락 | 내 버그 | 수정 · 스모크 검증 |
| 7 | 스테일 락 | 내 판단 착오 | resume 대기로 전환 |
| 8 | SSRF 가드 오탐 의심 | — | 가드가 옳았음 · 무수정 |
| 9 | 선택 판정 팩 v1 과엄격 | 내 설계 결함 | 팩 v2 발행 · 3그룹 사망 → 실패 이동 |
| 10 | 검색어 분산 | 내 설계 결함 | 주 구조물 좁힘 재검색 · 10/10 통과 |
TypeError: object of type 'NoneType' has no len() (text_cleaner.py:77)message.content 가 None 으로 왔다. finish_reason='length' — 출력 상한에 걸린 것으로 보인다.len(text or "") 로 막으면 대본 40페이지가 조용히 사라진 채 하류 전체가 오염된다. 대신 ①빈 응답을 EmptyExtraction 예외로 세우고 finish_reason 을 기록 ②페이지 범위를 반으로 쪼개 재시도(최대 5단계) ③1페이지까지 좁혀도 비면 세운다. 40페이지 이하 단일 호출 경로에도 같은 재시도를 붙였다.cleaned_text 61,705자 정상 추출, 분할 재시도 로그 0건 — 간헐적 사건이었으나 안전망은 남았다.[VERIFY-EXIT] step=scene_detail failed → partial: ['1 owned contract_violation: [(59, 12)]'] Step scene_detail partial and allow_partial_downstream=False — stopping pipeline
scene_detail_redo_service.redo_scene_detail_shot 으로 걸린 샷 하나만 재저작. 그 서비스 docstring 에 내가 빠진 함정이 문자 그대로 적혀 있었다.위반 [] / manifest status: completed / promoted=True. 40분 대신 수분.Foreign key associated with column 'entity_canon.project_id' could not find table 'project_registry'from app.models import catalog 명시 import. 설계 문서에 이미 적어둔 함정이 실제로 재현됐다.step_run_promoted=True.column "is_primary" is of type integer but expression is of type booleanImageAsset.is_primary 는 Column(Integer) 인데 Python False 를 넣었다. Postgres 가 거부한다.is_primary=0.transaction has been rolled back due to a previous exceptiondb.rollback() 추가.NotNullViolation: null value in column "created_at"created_at 은 NOT NULL 이고 서버 기본값이 없다. 기존 _upsert_seed_asset 은 넣고 있었는데 내가 빠뜨렸다.created_at=datetime.now(timezone.utc) + annotate_generated_asset 계보 주석 배선.Step outdoor_structure_form_reference error: 이미 다른 worker 에서 실행 중 decision=force_explicit, is_stale_steal=False
step_run.status='running' 이 남는다. 만료 판정은 started_at 기준 3600초이고, 락 회수(STALE_RUNNING_RECOVERY)는 resume 경로에서만 계산된다. mode='force' 는 그 경로를 아예 타지 않아 영원히 못 뺏는다.resume 모드로 1회 실행. 더 나쁜 것은 실패한 시도가 started_at 을 갱신한다는 점이다 — 2분 간격으로 25회 두드렸더니 경과가 3577s → 425s 로 리셋됐다. 두드릴수록 멀어진다. 올바른 대응은 아무것도 하지 않고 3600초를 넘긴 뒤 resume 1회.step_runner.py:700~735(만료 판정), 1025~1036(force 는 steal 불가).search_grounded_ref: 비공인 호스트 거부 d2k5miyk6y5zf0.cloudfront.netgetaddrinfo → nodename nor servname provided. 대조로 commons.wikimedia.org 는 정상 해석·통과.1.202607281800)쓸 만한 후보 없음 (VLM 선택 0) 으로 사망 — bg_forest_ritual_clearing · bg_natural_cave · bg_police_patrol_boat.“성인 키의 3~4배에 달하는 거대한 자연 바위와 그 바로 앞에 고정된 중앙 제단이 결합된 형태를 보여주는 사진이 없습니다” “자연 동굴 입구와 그 주위에 고정된 나무 기둥 지지대 구조가 함께 온전히 포함된 레퍼런스는 없습니다” “순찰정의 전체적인 종단 배치를 종합적으로 파악할 수 있는 이미지가 없습니다”
pick_system 이 장면 구성의 완전성을 요구했다. 그런데 판정 대상은 이 작품이 지어낸 장소다 — 그 구성 그대로를 찍은 사진은 웹에 존재할 수 없다. 형태 참조의 목적은 그 종류의 시설 하나의 구조·재질·비례를 읽는 것인데, 계약이 그 목적과 어긋나 있었다.2.202607291130 으로 발행하고 REF_PACK_VERSION_MAP·selector 까지 같은 단위로 배선했다(팩 덮어쓰기 금지 · 미배선 팩 발행 금지). 명문화한 것 — ①서술의 요소가 한 장에 다 나올 필요 없음 ②나머지 요소의 부재·배치 차이는 탈락 사유가 아님 ③완전 일치보다 읽히는 예시 우선 ④chosen_index=0 은 “주 시설 종류가 아예 없을 때”로 한정.goat_pen·main_pier·natural_cave). 실패가 옮겨간 덕분에 두 번째 원인(10번)이 드러났다.bg_natural_cave → “실내 전시관에 복원된 목조 구조물과 돌무더기 유구” / “지하로 파고든 방형의 목조 우물 또는 수혈 유구” bg_forest_goat_pen → “방역을 위한 양돈장 철제 출입문” / “현대식 축사(우사)의 측면 구조” / “소들이 수용된 현대식 우사”
structure_desc 는 복합 서술이다(“동굴 입구 + 덧댄 나무 기둥 + 목재 받침”, “염소 우리 + 가축 출입문”). 그 전체를 그대로 질의에 실으면 검색 엔진이 하위 요소의 지배적 주제를 밀어올린다 — 목재 지보는 고고학 유구로, 가축 출입문은 소 외양간으로 샜다. 회수가 빗나가 있으니 판정을 아무리 완화해도 고를 것이 없다.narrow_retry_hint.md 를 추가하고, 스텝에 선택이 0 이면 주 구조물 하나로 좁혀 1회 재검색하는 경로를 배선했다. 판정 완화(9번)와 짝으로만 작동한다 — 좁혀 다시 받아도 판정이 구성 완전성을 요구하면 또 0 이 된다.bg_natural_cave 검색 8라운드·질의 16건·회수 26장 → 선택 성공, bg_forest_goat_pen 2라운드·질의 6건·회수 6장 → 선택 성공. 좁히지 않은 그룹(bg_multifamily_residence)은 1라운드로 끝났다.bg_roadside_gas_station”)outdoor_structure_form_reference 10 그룹 vs outdoor_structure_seed 25 그룹. 차이 15개가 검색 참조 없이 생성됐다.인공 구조물 7 — bg_roadside_gas_station(주유소) · bg_convenience_store(편의점) · bg_police_station(파출소) · bg_forest_slaughterhouse(도축장) · bg_harbor_restaurant_deck(식당데크) · bg_reed_field_wooden_shed(목조창고) · bg_village_speaker_tower(스피커탑) 자연 지형 8 — coastal_cliff · coastal_rock_path · dream_forest_path · forest_entrance · forest_stone_markers · pine_forest_trail · wave_battered_rocks · winter_reed_field
structure_plate 바인딩 그룹과 exact parity 로 잡았다(설계 §8 B-1). 그래서 mandatory_group_ids 가 그 10개로 고정됐고, structure_plate 에 묶이지 않은 그룹은 조용히 대상 밖으로 빠졌다. 사용자 지시는 “야외 등 모든 요소 배경이 참조”였는데 프록시를 잘못 고른 것이다. 결함의 성격이 9·10번과 다르다 — 그 둘은 검색이 돌았는데 결과가 나빴던 것이고, 이건 검색이 아예 돌지 않았다.3.202607291430 + scope_system.md), search_grounded_ref.py 의 load_scope_system/build_scope_schema/validate_scope_output, 스텝의 _judge_scope/_group_fingerprint/_reusable 와 합집합 대상까지 구현돼 있다. 적용을 미룬 이유는 두 가지다 — ①적용 시 config_hash drift → force → clear_checkpoint 로 form_reference manifest 를 날린 사고가 07-29 에 있었다 ②15개 씨드를 다시 만들면 그 씨드를 쓴 배경, 그 배경을 쓴 최종 샷까지 연쇄해 사실상 전면 재실행이라 마감(07-30 14:00)을 지킬 수 없다.shot_conti_light 가 partial(91 적용 / 88 성공 / 3 실패). S1sh7·S41sh2·S65sh4 가 레인 마커 맵 시야각 쐐기 작화로 3라운드(17:35·23:16·01:05) 모두 실패했다. 사용자가 “무콘티로 진행”을 택했는데, 그 하나를 위해 서로 다른 층의 게이트 3개를 차례로 만났다.① _require_cp_data — 매니페스트 status=='completed' 요구 ② 샷별 lane 게이트 — lane_conti[tag] 있는데 status!='ok' 면 실패 ③ BGFIRST2 — 정책 대상 샷의 콘티 결손은 legacy 하강 금지
bgfirst_no_conti_exempt_tags(쉼표 구분 샷 태그). 빈 값(기본)이면 기존과 byte-identical 이고, 선언된 태그에만 적용되며 적용 시 WARNING 을 남긴다. ①②는 이번에 manifest_operator_no_conti_backup_20260730_015226.json 으로 원본을 아카이브한 뒤 실패 엔트리를 data.lane_conti_operator_skipped 로 옮기고 operator_override 블록을 기록하는 방식으로 처리했다. failed_count=3 과 DB partial 은 지우지 않았다(정직한 기록).운영자 선언 무콘티 예외 — BGFIRST 대상 제외, 기존 경로로 생성 로그와 함께 통과, scene_image_pipeline completed 7/7 실패 0. ★재리뷰 정정(07-30) — 예외를 실제로 소비한 샷은 S1sh7·S65sh4 둘뿐이다. S41sh2 는 콘티는 실패했지만 스틸 단계에서 BGFIRST 정책 대상이 아니어서 본 실행에서 그냥 생성됐다. 즉 세 게이트를 다 만나는 샷과 그렇지 않은 샷이 섞여 있었고, 나는 처음에 셋을 같은 문제로 묶어 봤다. 대상 판정은 샷별로 다르다. 남은 과제 — ①의 operator override 를 코드로 정식화하는 일은 아직 안 했다. 지금은 매니페스트 편집이 유일한 길이다.__bgfirst_bg 배경 재투영 2.0~2.1분 (씬 첫 샷에만) 롤 _a/_b/_c 0.2~0.3분 (이미 병렬 — 줄일 여지 없음) _fix 결함 수정 i2i 평균 1.58분 (매 샷) _sel 선정 평균 0.80분LLM 호출 실시간은 벽시계의 24%뿐이었다.
still_recipe_critique_enabled(결함 검사 → 수정본 i2i)였다. 끄면 샷당 약 1.9분이 빠진다.resume 1회로 회수 — 완료분 13샷 손실 0. 실측 결과 샷당 2.01분(예측 2.2분), 완주 10:22 로 약 5시간 50분을 회수했다._fix 산출 13샷 / 237. 즉 224샷은 결함 검사·수정을 거치지 않았다. 게다가 still_recipe_critique_enabled 가 _config_hash_base 페이로드에 들어 있어(image_steps.py:632) critique 를 켠 표적 재저작은 불가능하다 — base hash 가 달라져 표적 drift 로 분류되지 못하고 BLOCK+force 요구가 되는데, force 는 표적과 병용 금지다(교착). 육안 판정 시 이 224샷은 선정 3롤 중 최선이지 수정본이 아니라는 점을 감안해야 한다.SCENE_IMAGE_TARGET_SCENES 를 바꾸면 config_hash 가 달라져 mismatch 가 난다. 이미 구운 234샷이 무효화될 위험._config_hash 에 접히지만 _config_hash_base 에는 접히지 않는다 — 그 차이가 ‘표적만 바뀐 drift’를 식별하는 기준이다._evaluate_contract_drift 가 RERUN_SELF(cleanup/invalidate/clear 없음)로 분류.[RECOVERY] allowlist contract_drift cycle=1: target-scope drift only (base hash 동일) — 비파괴 resume 재실행이미 구운 샷은 하나도 다시 굽지 않았고, 표적 7샷만 처리됐다.
ModerationError: Content moderation blocked: text_refusal:STOP. sanitize 재시도 로그 40건(본 실행 37 · S61sh1 재시도 3).S61sh1 하나만 본 실행에서 3회를 소진해 실패했고, 표적 재시도(159초)에서 다시 3회 sanitize 를 거쳐 통과했다.import 해 s7 문제 보고서 탭으로 렌더하고 있었다. 사용자: “문제보고서는 노출되면 큰일나니 합치지 말고”.STAGES 에서 s7 제거. tab_incident() 함수는 남겨두되 호출하지 않는다.유실 185 를 찍는다 ②이미지 237장을 만들었는데 scene_still.image_generated 가 237건 전부 false / pending.overwritten 상태이고 단계는 round1(98) + round2(87) — 즉 이전 라운드 검색 후보가 나중 라운드에 덮어써져 sha 가 달라진 것이다. 현재 산출 단계(검색·씨드·배경·콘티·최종샷)의 유실은 0. ②는 파이프라인이 쓰는 값이 아니라 백엔드 startup 이 backfill 하는 self-healing 플래그다(database.py:140). 헤드리스 러너로만 돌려 갱신 기회가 없었을 뿐이다.image_generated=t 237건. 교훈 — 이상 신호를 발견하면 결함으로 단정하기 전에 그 값을 누가 쓰는지부터 확인할 것. 나는 두 건 다 “제출물에 구멍”으로 먼저 의심했고, 둘 다 아니었다.미실행 으로 표시. 실제로는 237장이 디스크에도 DB 에도 있었다.scene_image_pipeline 체크포인트의 data.shots / data.scenes 를 읽고 있었는데 그런 키가 애초에 없다. 실측한 data 는 {scene_total, primary_count} 두 개뿐이다. 게다가 표적 재실행 뒤에는 그 3샷만 반영해 개수의 정본으로도 쓸 수 없다. ‘있을 법한 키’를 확인 없이 가정한 내 잘못이다.scene_still ⋈ image_asset)로 옮겼다. 관람용 빌더(DB 접근 가능)가 shots_index.json 을 저작하고, 제작 과정 갤러리(표준 python3, DB 미접근)가 그것을 읽는다.진행 중 · 91. 러너는 이미 종료했고 88/91 로 끝난 상태였다.status 를 completed 로 override 해 둔 것을 그대로 읽었다. 즉 내 override 가 실패 사실을 가리고 있었다.failed_count / operator_override 로 바꿨다. override 를 ‘완료’로 미화하지 않는다. 이때 override 하면서 failed_count=3 을 지우지 않고 남겨둔 것이 근거가 됐다 — 정직한 기록을 남긴 것이 나중에 실제로 쓰였다.일부 실패 · 3샷 실패(S1sh7·S41sh2·S65sh4 — 레인 마커 맵 시야각 작화) 로 사실대로 표시.http://192.168.35.42:8935/. 외부 서버로 옮기면 사내망 주소라 열리지 않는다.../04_process/ 로 교체. 서버 배치 시 폴더명을 ASCII 로 올리므로 그 사본에만 이름을 맞췄다(공개 URL 에서 한글 퍼센트 인코딩이 메신저·메일에서 깨지는 것을 피하려는 것 — 한글 경로 자체는 200 으로 동작 확인).https://jedi.team/g2/01_gallery/ 에서 링크 이동 정상. 전 페이지 외부 이미지·CSS·JS 0개로 자기완결 확인. 같은 점검에서 ZIP 안 랜딩이 웹 랜딩보다 구버전인 것도 잡아 ZIP 전용 랜딩(다운로드 카드 제거)으로 분리했다.started_at 을 갱신해 만료가 계속 미뤄진다. 3600초를 조용히 넘긴 뒤 resume 으로 1회.force 가 resume 보다 강하다는 가정을 코드로 확인하기 전에 쓰지 마라. 이 코드베이스에서는 두 경로의 관할이 다르다.scene_detail 계약 위반에 전체 재실행 금지. 매번 다른 샷이 걸린다. 전용 redo 서비스를 쓴다.is_primary=0·created_at·annotate_generated_asset 이 필수다. 재개 전에 스모크 INSERT 로 확인하는 편이 싸다.app.models.catalog 를 명시 import.structure_plate 바인딩은 10개, all_groups 는 25개 — 동치가 아님이 실측으로 확인됐다. 초안대로 LLM 이 검색 대상을 한 번 더 걸렀다면 어선·경비정·선착장 같은 구조물이 조용히 순수 T2I 로 남았을 것이다(Codex BLOCKING-1 수용의 실효).이 문서는 다음 세션이 같은 함정에 다시 빠지지 않는 것을 목적으로 하며, 내가 틀렸던 판단(4·5·6·7·9·10)과 의심이 빗나갔던 건(8)을 그대로 기록했다. 각 라운드의 후보 사진과 반려 사유 원문은 제출 갤러리의 “이전 라운드 감사 이력” 절에 있다.