POSA_Copyrighter/docs/reviews/2026-08-08-utility-assessment.md
changukyu 847f7a852d fix: 게시 호스트를 영속화에서 복구하고 콘솔·LLM 에 노출
P1 3번("매칭 호스트 기반 신뢰출처 구분")을 착수하며 전제를 검증했더니 틀렸다.
호스트는 "이미 저장돼 있고 점수만 안 쓰는" 게 아니라 저장 시점에 파괴되고
있었다.

  google_custom_search.py:240,296 이 실호스트(displayLink)를 data["domain"] 에
  넣는데, store_serialization.py:298 이 data["provider"] 를 우선하므로 그 값이
  API 이름으로 덮어써진다. Vision·Naver 는 호스트를 아예 안 넣고 URL 만 준다.
  결과적으로 저장된 모든 행의 domain 이 "google" 이고, LLM 프롬프트 12줄이 전부
  domain=google 이었다 — 저작권 심사에서 가장 결정적인 "어디에 떠 있나"를 모델도
  운영자도 볼 수 없었다. 같은 케이스의 payload 55행 중 41행이 url 을 갖고 있는데도.

domain 필드는 재사용할 수 없다. provider 커버리지 동기화와 CSE 쿼리 시그니처
매칭이 그 값을 키로 쓰고(store_serialization.py:195, 599) 콘솔도 provider
라벨로 렌더한다. 그래서 host 를 별도 키로 신설했다.

  _result_host(): data["domain"] 이 provider 와 다르면 그것을, 아니면 결과/페이지
  URL 에서 호스트를 파싱한다. www 제거·소문자화. 파싱 실패는 "호스트 없음"이지
  영속화 실패가 아니다.

  점수에는 영향이 없다 — RiskScorer 는 domain 도 host 도 읽지 않는다. 증거 id 도
  안 바뀐다: _evidence_id 는 도메인 객체의 data 를 해싱하고 payload 를 해싱하지
  않으므로 payload 키 추가가 id 를 움직이지 않는다.

  PROMPT_DATA_FIELDS 의 domain -> host. 화이트리스트 크기는 그대로.

  콘솔은 증거 카드 제목 줄에 monospace host-chip 을 붙인다. 기존 저장 행에는 host
  키가 없으므로 evidenceHost() 가 저장된 URL 에서 파생한다 — 292행 마이그레이션도,
  bootstrap 마다 행당 파싱도 필요 없다(렌더되는 행만 지나간다).

실렌더 검증(127.0.0.1:9512, 실 DB)에서만 드러난 레이아웃 결함 2건을 고쳤다.
코드 리딩으로는 보이지 않았다.
  1) 긴 CDN 호스트(cloudfront-ap-northeast-1.images.arcpublishing.com)가 제목 줄
     폭을 다 먹어, evidence-title 의 overflow-wrap:anywhere 와 결합해 제목이 한
     글자씩 세로로 깨졌다. 칩을 max-width 20ch 로 묶고 제목에 flex:1 1 auto 를 줘
     남는 폭을 갖게 했다.
  2) 그 수정 후에는 반대로 biz.chosun.com(14자)까지 잘렸다. 칩을 flex:0 0 auto 로
     고정했다 — 제목("구글 full image match found")은 전 행이 동일한 상용구이고
     호스트가 그 행의 실제 내용이므로, 들어갈 수 있는 호스트가 잘리는 쪽이 잘못이다.
  최종: 칩 82개 전부 유효 호스트 형태(TLD com/edu/hk/jp/kr/net/org), 가시 칩 41개 중
  잘리는 것 4개(모두 20자 초과 CDN), 콘솔 에러 0.

PROVENANCE 배점은 보류한다. 두 조건이 다 안 맞는다. (a) detect_screen_capture 의
카테고리가 geometry 와 subtitle_band 둘뿐인데 2개를 요구하므로 라벨의 실제 의미는
"geometry AND 자막"이고, 자막 없는 방송 캡처는 구조적으로 도달 불가다 — 임계를
낮추는 게 아니라 독립적인 세 번째 신호가 필요하고 그건 새 탐지기다. 한계를 실측과
함께 상수 옆에 기록했다. (b) "새 탐지 신호는 0점 라벨로 출시한 뒤 decision_stats
상관을 보고 가중치 부여"가 2026-07-25 결정이고, 판정 0건인 지금 배점하면 그 결정을
위반한다.

신뢰출처 판정 자체는 "어느 도메인이 우리 것/라이선스 계약처인가"라는 운영자
데이터가 있어야 한다. 호스트가 살아났으므로 목록만 정해지면 cleared_reference 와
같은 패턴(점수 0 + 사유 노출)으로 붙는다.

테스트 4건 추가. pytest 574 passed(착수 전 570), 린트 클린.
2026-08-09 08:51:12 +09:00

20 KiB
Raw Blame History

기능 실효성 평가 — "이 도구가 실제 업무에 도움이 되는가"

2026-08-08 · 6축 조사 + 축별 적대적 반증(에이전트 12). 모든 수치는 실코드·실DB·실모델 실행 결과다.

결론

검색·기록 노동은 실제로 줄여준다. 심사할 건수는 전혀 줄여주지 못한다. 현재 이 도구는 위험 판별기가 아니라 증거 수집·기록기다.

축 판정
증거 수집 자동화 작동 — 단, 탐지력 이득이 아니라 배치·영속화·감사 이득
위험 점수 판별력 미작동 — 전건 high 수렴, 우선순위 정보 0비트
LLM 증거 판독 격리는 성공, 산출물 가치는 미검증 — 운영 DB 기준 한 번도 표시된 적 없음
콘솔 작업 경제성 승인/보류 루프만 빠름 — 반려·일괄·감사는 병목
운영 준비도 폐쇄망 반입·백업은 견고, 신원·처리량은 미구현

1. 실제로 도움이 되는 것 (반증 통과)

  • Cloud Vision 웹탐지가 신규 제출 전건에 자동 1회 실행되고 결과가 케이스에 영구 저장 + 감사기록으로 남는다 (integrations/cloud_vision_web_detection.py:39-98, jobs/batch_analyzer.py:77-84, sqlite_store.py:430-446). 탐지 인덱스는 운영자가 Lens에 직접 넣는 것과 동일하므로 이득은 탐지력이 아니라 배치·영속화·감사다.
  • 승인/보류 키보드 1타 + 자동 다음 케이스 이동 (app-keyboard-batch.js:182-200). 저위험 대량 승인 루프는 실제로 빠르다.
  • 중복 탐지가 실증적으로 일한다 — 재저장(JPEG q90)/80%축소/50%축소/밝기+10% 변형 24케이스 중 24건 그룹핑, 같은 24케이스에서 SHA-256 완전일치는 0건. 지각 해시 레이어가 exact 해시가 못 잡는 재제출을 잡는다. 갭은 5% 크롭 6/6 미탐(해밍 2~5비트 vs 임계 0.97의 1비트 예산).
  • 판정 시점 스냅샷 · 메모 없는 반려 차단(서버+클라이언트) · 대상별 내보내기 화이트리스트 (store_workflow.py:43-85, :21-37 APPLICANT 5필드 vs INTERNAL 10필드).
  • 백업/복구 왕복 실증 성공 (Connection.backup + pre-restore 구제 사본 + 사이드카 제거).
  • 폐쇄망 Python 반입 절차 완결 — 런타임 서드파티 3개 핀 고정, GUI CDN 참조 0건, Pretendard 동봉, YuNet 가중치 실물이 문서 표의 232,589 bytes와 일치.
  • SSRF 가드 실동작, 쿼터 원자 예약(try_reserve/release), 쿼리 시그니처 dedup으로 재분석 시 Naver/CSE 재소모 0.
  • LLM이 점수·밴드를 절대 움직이지 못하도록 격리 (risk_scoring.py:89, 101-102). Ollama 부재 시 읽을 수 있는 failure 행 1건으로 끝나고 점수 영향 없음(실측 2.01s / 0.19s).

2. 판별력 붕괴 — 이 도구가 "심사 건수"를 못 줄이는 이유

신호조합 224개 전수 계산: high 174건(77.7%) / medium 42건(18.8%) / low 8건(3.6%), 126건(56.2%)이 정확히 100점. 실 DB 6건도 100×4 / 45×2, low 0건.

지배적 동인은 match 타입 스택이 아니라 web entity 누적이다 (analysis/risk_scoring.py:159-160, 168-174). 버킷이 entity 이름별로 분리돼 서로 dedup되지 않고 round(40 × confidence)씩 쌓인다. Vision은 maxResults=10 (cloud_vision_web_detection.py:47)이라 모든 이미지가 라벨 10행을 받는다. 실측:

입력 결과
entity 2개만 (이미지 매칭 0건) 72 / high
entity 3개만 (이미지 매칭 0건) 100 / high
웹에 전혀 없는 오리지널 사진 (dog/pet/grass) 66 / medium
full+partial+visual+page 각 1건 (평범한 웹 존재) 100 / high
entity4 + full + partial + visual + page + 얼굴 (명백 침해) 100 / high

이 누적은 사고가 아니라 테스트가 고정한 동작이다 — test_risk_scoring.py:234-258이 "서로 다른 엔티티 2개는 반드시 high로 쌓여야 한다"를 assert한다. 따라서 기존 테스트로는 절대 잡히지 않는다.

파생 결과:

  • 큐 기본 정렬이 riskScore 내림차순(app-selectors.js:304)인데 전건 100이면 정렬이 no-op다.
  • low 밴드가 사실상 도달 불가 — low 8건은 전부 "아무것도 못 찾음" 조합뿐이라 "안 봐도 된다"는 결론을 못 준다.
  • 신호 우선순위 역전 — 법적으로 가장 결정적인 EXIF/IPTC 권리 표기가 0점이라 (c) Getty Images 단독 제출은 0점/low인데, Vision의 추측 라벨 3개는 100/high다 (risk_scoring.py:80-88 PROVENANCE는 reason만 남기고 continue).
  • 운영자 되먹임이 순증 노동 — irrelevant 표시는 버킷당 "점수 매기는 첫 미표시 행"만 밀어내므로, 실 DB 제출물 4번을 100→35로 내리려면 google 증거 51행을 개별로 표시해야 한다. 일괄 API가 없다(개별 /api/evidence/{id}/status 뿐).
  • cleared_reference가 재심사를 못 막는다 — FINGERPRINT 분기에서만 0점이라 [cleared_reference + 평범한 Vision 응답] = 100 high. 외부 호출 쿼터도 그대로 소모한다.

정답 라벨 0건: submissions 6건 전부 unreviewed, corrections 0행, Operator decision created 감사 이벤트 0건. 밴드 임계 70/30이 실제 판정과 한 번도 대조된 적이 없다.

3. 검증 코퍼스가 없다 (전제 정정)

data/copyrighter.sqlite3의 evidence 292행 중 288행이 2026-05-26, 이 리포의 첫 커밋은 2026-06-09이다. 즉 DB는 git 이력보다 2주 앞선 구빌드 스냅샷이며 6~8월 두 달치 변경이 반영돼 있지 않다:

  • 저장 entity 행 40개 전부 entity=null → 리스코어 시 0점 (현행 코드는 저장함)
  • 저장 entity confidence가 14.862 (현행 코드는 [0,1] 클램프)
  • 증거 292행 전부 summary/domainSource/faceTier/verified/groundedClaims 키 부재
  • submissions 6건 전부 scorerVersion 부재, submittedEpoch=0

판별력 판정을 확정하려면 실이미지 30~50장을 현행 코드로 새로 돌려야 한다. 그것이 밴드 캘리브레이션의 출발점이기도 하다.

4. 이번에 수정한 것 (P0)

(1) 폴더 정리가 판정 기록을 지우던 문제 — 확정 후 수정

실증: record_decision('3','rejected') → export 1행 → 원본 파일 제거 → _prune_missing_submission_files → submissions 0행 / evidence 0행(cascade) / export 0행.

sqlite_store.py:_prune_missing_submission_files에 판정 완료 가드를 넣었다. 판정된 케이스는 원본 파일이 없어도 보존하고 assetMissing 마커 + 감사 이벤트를 1회만 남긴다(매 리로드마다 재기록되면 500행 감사 캡을 먹는다). 미판정 케이스의 프루닝 동작은 그대로다.

(2) 비상 차단 스위치가 장식이던 문제 — 확정 후 수정

대조실험: 스텁 어댑터로 계수 → CONTROL 5회 / emergency_disable_external_providers() 후 5회로 동일. 원인은 스위치가 DB 플래그만 쓰고 자동 보강 경로가 그 플래그를 아예 읽지 않은 것. 게이트는 수동 검색에만 있었다.

_external_calls_blocked(provider_id)를 신설(store_enrichment.py)하고 자동 외부 호출 4개 경로 전부에 배선했다:

경로 provider
_auto_naver_search naver
_auto_google_custom_search google_search
_rerun_google_image_search google
_google_face_crop_web_detection google (생체 전송이라 상위 게이트에 의존하지 않고 독립 게이트)
시드 경로 Vision 배치 (sqlite_store.py) google → ExternalApiPolicy(disabled=True)

게이트는 enabled가 아니라 operatorDisabled 를 본다. 전자는 연결성/헬스에도 반응하므로 운영자가 멈추라고 한 적 없는 트래픽까지 죽인다. 후자는 개별 토글과 비상 스위치만 세우는 의도적 정지이고 리로드에도 유지된다.

(3) 생체정보(얼굴 크롭)가 보존기간을 초과 생존하던 문제 — 확정 후 수정

두 경로가 있었다: (a) 스냅샷이 face-crops를 통째로 담는데 프루닝은 나이가 아니라 개수 10개 기준이라 90일에 파기됐어야 할 크롭이 암호화 없는 zip 안에 남았다. (b) 복구는 copyfileobj로 새로 쓰므로 mtime이 리셋되고, purge가 mtime을 보므로 파기 대상이 복구 직후부터 다시 90일을 산다.

store_backup.py의 _MEDIA_SOURCES에서 face-crops를 제외하고, 복구 시에도 _RETENTION_BOUND_MEDIA로 레거시 아카이브의 크롭을 건너뛴다. 크롭은 원본 이미지에서 재분석으로 재생성되므로 손실 비용이 없다.

(4) 오프라인 설치 문서가 계약 위반 모델을 지목하던 문제

offline-install.md가 qwen2.5:0.5b-instruct 반입을 지시했다 — 같은 리포가 55.7초 + 프롬프트 형식 예시 반향으로 출력 계약 위반 기록을 남긴 모델이다. 문서대로 반입하면 타깃에서 요약이 조용히 죽는다. qwen2.5:7b로 정정하고, ollama 바이너리 반입 누락·GPU 전제·"env 비우면 LLM 꺼진다"는 거짓 서술 (실측: build_provider_runtime({}) → enabled=True, compliance 문자열까지 참으로 표시)을 함께 고쳤다.

검증: .\verify.ps1 — ruff 클린, pytest 565 passed(착수 전 562, 신규 회귀 테스트 3건).

5. 남은 우선순위

P1 — 판별력 복구 (이것이 "업무 도움" 여부를 가르는 축)

  1. web entity 누적 캡 → 완료(2026-08-09, SCORER_VERSION v1→v2). 아래 6절 참조. 조사 단계의 "test_risk_scoring.py:234-258이 현 동작을 고정한다"는 예측은 틀렸다 — 그 테스트는 category가 붙은 엔티티(+45 분기)를 다루고, 포화를 만든 것은 category 없는 raw 라벨(+round(40*conf) 분기)이다. 두 분기를 갈라 라벨만 캡했으므로 그 테스트는 개정 없이 통과한다.
  2. PROVENANCE(EXIF/IPTC 권리 표기)에 배점. 단 실이미지 56장에서 증거 행 0건이었고, 방송캡처 분기는 MIN_CAPTURE_CATEGORIES=2에서 geometry가 1개 카테고리라 자막 없는 방송 캡처는 구조적으로 도달 불가다 (provenance.py:85-88). 보류 — 조건이 둘 다 안 맞는다. (a) 카테고리가 geometry와 subtitle_band 둘뿐인데 2개를 요구하므로 라벨의 실제 의미는 "geometry AND 자막"이다. 임계를 낮추는 게 아니라 독립적인 세 번째 신호(채널 로고·하단 띠)가 필요하고 그건 새 탐지기다. 한계를 실측과 함께 provenance.py 상수 옆에 기록해 두었다. (b) "새 탐지 신호는 0점 라벨로 출시한 뒤 decision_stats 상관을 보고 가중치 부여"가 2026-07-25 사용자 결정이다. 판정 0건인 지금 배점하면 그 결정을 위반한다. 순서는 분기 수정 → 0점 출시 → 상관 → 배점.
  3. 매칭 호스트 기반 신뢰출처 구분 → 전제가 틀렸다(7절). 호스트는 "저장돼 있는데 점수만 안 쓰는" 게 아니라 영속화 시점에 파괴되고 있었다. 파괴를 먼저 멈췄다.
  4. 밴드 재캘리브레이션은 판정 30~50건이 쌓인 뒤. decision_stats 교차표는 이미 구현돼 있다.

P2 — 운영

  1. 운영자 신원. 현재 감사 actor가 전부 하드코딩 리터럴(rights.ops/admin.ops/system)이라 "누가 반려했나"에 답할 수 없다.
  2. 레거시 llm 행 마이그레이션. summary 키가 없는 9행이 _provider_item_has_result를 통과해 only_if_missing 경로를 막으므로 6개 제출물 어디에도 요약이 영원히 생성되지 않는다. 커버리지 패널은 "했다"고 보고한다.
  3. LLM 프롬프트의 domain이 실호스트가 아니라 provider 이름(store_serialization.py:298이 data['provider']를 우선). 저작권 심사에서 가장 중요한 "어디에 떠 있나"를 모델이 원천적으로 말할 수 없다. 같은 케이스 55행 중 41행이 url을 갖고 있는데 프롬프트 12줄 전부 domain=google이다.
  4. 자동 쿼리 3슬롯을 페이지 제목이 전부 선점(우선순위 100 vs entity 88/82)해 권리 지향 쿼리가 구조적으로 실행되지 않음
    • _clean_title이 말줄임표를 제거하지 않아 저장 제목 39개 중 13개(33%)가 잘린 채 검색어가 된다.
  5. 판정당 bootstrap 전량 왕복 2회. 300케이스 실측 9.58MB, 유효 델타는 36KB. POST /decision 응답이 이미 완결된 케이스 페이로드인데 버리고 /api/bootstrap을 다시 친다.
  6. 반려 경로의 키보드 단절(제출 단축키 없음), 감사 로그 500건 고정 상한 + 필터·내보내기 부재.

6. P1 1차 — 신호 풀 도입 (2026-08-09, SCORER_VERSION v1 → v2)

포화의 동인은 하나가 아니라 둘이었다. 둘 다 "같은 사실을 여러 행으로 세는" 같은 형태다.

풀 상한 무엇을 묶나 근거
WEB_ENTITY_MAX_POINTS 20 (medium 30 미만) category 없는 raw Vision 라벨 Vision 은 모든 이미지에 라벨을 최대 10개 붙인다. 모집단 전체에 상수로 존재하는 신호는 판별에 기여할 수 없다
WEB_PRESENCE_MAX_POINTS 60 (high 70 미만) full / partial / page / visual / url 응답 1회가 "이 이미지가 웹에 있다"는 하나의 사실을 4개 상관된 뷰로 보고한다. 합산은 45+35+35+10=125 로 상한을 넘겨 웹에 조금이라도 있으면 전부 100 이 됐다

category(character/celebrity/work)가 붙은 엔티티는 풀에서 제외했다 — 그것은 raw 라벨이 아니라 권리 분류이고 엔티티별 +45 를 유지한다.

설계 원칙: 웹에 존재한다는 사실만으로는 high 가 되지 않는다. high 가 되려면 두 번째 종류의 신호 (인물, 지식DB 지문 매칭, 승격된 검색 히트)가 필요하다. 존재 자체는 침해가 아니고, 그 판단이 운영자의 몫이다.

신호조합 224개 전수 재측정 (v1 → v2):

케이스 v1 v2
웹에 없는 오리지널 사진, 라벨만 100 / high 20 / low
팀이 이미 안전 확인한 자산(cleared) 100 / high 20 / low
인물 사진, 라벨만 100 / high 55 / medium
웹 존재 광범위(4개 매치 타입) 100 / high 60 / medium
웹 존재 + 배경 인물 100 / high 70 / high
완전일치 사본 + 식별가능 인물 80 / high 80 / high
지식DB 매칭 + 인물 100 / high 100 / high
명백한 침해(전 신호) 100 / high 100 / high

다른 신호 없이 웹 신호만으로 high 가 되는 조합: 2/7 → 0/7. 100점 포화 조합 66.5% → 51.3%.

정직한 한계: 224조합 전수의 high 비율은 82.6% → 76.3% 로 크게 안 움직인다. 이 격자는 신호 축을 균등 샘플링하므로(지식DB 강매칭이 1/4을 차지) 실제 큐 분포의 대리값이 아니다. 실 DB 6건도 100×4 → 95×4 로 밴드가 바뀌지 않는다 — 그 4건은 전부 "웹에 완전일치 사본 + 인물"이라 high 가 맞다. v2 가 고친 것은 거짓 high (웹에 없음/라벨만/이미 안전 확인)이지, 진짜 high 를 줄인 게 아니다.

풀 상한 60/20 은 신호의 구조적 성질에서 나온 값이지 캘리브레이션 결과가 아니다. 판정 30~50건이 쌓이면 decision_stats 교차표로 임계와 함께 재조정해야 한다.

7. P1 2차 — 게시 호스트 복구 (2026-08-09)

P1 3번의 전제("호스트는 이미 저장돼 있고 점수만 안 쓴다")는 틀렸다. 실제로는:

  • google_custom_search.py:240,296이 실호스트(displayLink)를 data["domain"]에 넣는다.
  • store_serialization.py:298이 data["provider"]를 우선하므로 그 호스트가 저장 시점에 덮어써진다.
  • Vision·Naver는 호스트를 아예 안 넣는다. URL만 있다.
  • 결과: 저장된 모든 행의 domain이 API 이름(google)이고, LLM 프롬프트 12줄이 전부 domain=google이었다. 저작권 심사에서 가장 중요한 "어디에 떠 있나"를 모델도 운영자도 볼 수 없었다.

domain 필드는 재사용할 수 없다 — provider 커버리지 동기화와 CSE 쿼리 시그니처 매칭이 그 값을 키로 쓰고 (store_serialization.py:195, 599), 콘솔도 provider 라벨로 렌더한다. 그래서 host를 별도 키로 신설했다:

  • _result_host() — data["domain"]이 provider와 다르면 그것을, 아니면 결과/페이지 URL에서 호스트를 파싱. www. 제거, 소문자화. 파싱 실패는 "호스트 없음"이지 영속화 실패가 아니다.
  • 점수에는 영향 없다(RiskScorer는 domain도 host도 읽지 않는다). 라운드트립 불변식 유지.
  • _evidence_id는 도메인 객체의 data를 해싱하고 payload를 해싱하지 않으므로 기존 증거 id가 바뀌지 않는다.
  • PROMPT_DATA_FIELDS의 domain → host. 화이트리스트 크기는 그대로.
  • 콘솔: 증거 카드 제목 줄에 monospace host-chip. 기존 저장 행에는 host 키가 없으므로 evidenceHost()가 저장된 URL에서 파생한다 — 292행 마이그레이션도, bootstrap마다 행당 파싱도 필요 없다.

실렌더 검증(127.0.0.1:9512, 실 DB): 케이스 1건에서 호스트 칩 82개 전부 유효 호스트 형태(TLD com/edu/hk/jp/kr/net/org), 콘솔 에러 0. 예: biz.chosun.com, thumb.mt.co.kr, cloudfront-ap-northeast-1.images.arcpublishing.com.

실렌더에서만 드러난 결함 2건을 고쳤다 — 코드 리딩으로는 안 보였다:

  1. 긴 CDN 호스트가 제목 줄 폭을 다 먹어 제목이 한 글자씩 세로로 깨졌다(overflow-wrap: anywhere와 결합). → 칩을 max-width: 20ch로 묶고 제목에 flex: 1 1 auto를 줘 남는 폭을 갖게 했다.
  2. 그 수정 후에는 반대로 biz.chosun.com(14자)까지 잘렸다. → 칩을 flex: 0 0 auto로 고정. 제목("구글 full image match found")은 전 행이 동일한 상용구이고 호스트가 그 행의 실제 내용이므로, 들어갈 수 있는 호스트가 잘리는 쪽이 잘못이다. 최종: 가시 칩 41개 중 잘리는 것 4개(모두 20자 초과 CDN).

남은 것: 신뢰출처 판정 자체. 이건 "어느 도메인이 우리 것/라이선스 계약처인가"라는 운영자 데이터가 있어야 하고, 그 목록을 .env에 둘지 지식DB 항목으로 둘지는 운영 워크플로 결정이다. 호스트가 살아났으므로 목록만 정해지면 cleared_reference와 같은 패턴(점수 0 + 사유 노출)으로 바로 붙는다.

적대적 검증에서 반증된 항목 (되돌리려는 시도 방지용 기록)

  • 폐쇄망에서 증거 썸네일이 안 보인다 → 런타임 외부 호출은 허용된 설계이고 http_app.py:510-511에 근거 주석이 있다. 다만 서버 egress와 브라우저 egress는 다른 문제이므로 납품 전 타깃망에서 썸네일 1장 실로드 확인은 남긴다.
  • 중복 그룹핑 backfill이 미작동 → reload/import-folder/upload 3개 라우트 전부가 backfill을 거친다. 배지가 안 뜬 건 테스트 이미지 6장이 서로 다른 사진이라서다.
  • 재분석이 Naver/CSE 쿼터를 재소모 → 시그니처 dedup으로 0콜. 실제 재소모는 Vision 쪽(dedup 없음 + 얼굴크롭 최대 3콜).
  • 쿼터로 스킵된 쿼리가 영구 등재돼 재시도 불가(7/24 H-3) → _RETRYABLE_SKIP_DOMAIN_SOURCES로 이미 수정됨.
  • 판정 내보내기가 decidedAt/memo를 빈 값으로 낸다 → 현행 경로는 정상. 실 DB의 공백 63행은 전부 비판정 이벤트다.
  • TLS 부재가 취약점 → 기본 바인드 127.0.0.1, 0.0.0.0 참조 0건. 미착수로 문서화된 항목이지 숨은 취약점이 아니다.