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