POSA_Copyrighter/docs/brainstorms/2026-07-25-ai-capability-expansion-brainstorm.md
changukyu 6bbaaf6e23 fix: 형식 예시 반향 차단, 기본 모델을 실측으로 qwen2.5:7b 로 확정
실모델 프로브에서 인용 검증만으로는 못 잡는 실패 모드가 드러났다.

## 형식 예시 반향(template echo)

qwen2.5:0.5b 는 프롬프트의 형식 예시를 그대로 돌려주면서(claim text 가
"<한국어 문장>") 실제 라벨 12개를 전부 인용했다. 집합 포함 검사는 이것을
완벽히 통과시킨다 — 인용은 전부 진짜이기 때문이다. 결과적으로 완전히 비어
있는 출력이 "근거 있음" 판정을 받았다.

그라운딩 검증은 "인용이 진짜인가"와 "내용이 진짜인가"를 둘 다 봐야 한다.
형식 예시 문자열을 llm_prompts 의 상수로 두고 그 반향을 거부한다. 앵글
브래킷으로 감싼 다른 스탠드인도 같은 실패로 취급한다.

가짜 transport 테스트로는 이 결함을 잡을 수 없었다 — 스크립트된 응답은
언제나 잘 만들어진 JSON 이기 때문이다.

## 기본 모델

저장된 실제 증거(제출물 #3, 54행 중 12행 선별, 프롬프트 1,507자)로 GPU
타깃에서 측정:

  qwen2.5:0.5b-instruct  55.7s  형식 예시 반향, 계약 위반
  qwen2.5:7b              8.8s  계약 준수, claim 2 + 확인필요 1   <- 채택
  qwen3:8b               35.7s  계약 준수, thinking 모델이라 4배 느림

3b 는 빌드 머신에 없고 7b 는 이미 있어 에어갭 반입 비용이 0 이다. 브레인스토밍
문서의 "7B 는 인터랙티브 불가" 는 CPU 전제였고 이 배포에는 해당하지 않는다.

pytest 517 passed.
2026-07-25 23:48:23 +09:00

418 lines
68 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 2026-07-25 AI 기능 확장 브레인스토밍
> 세션: 5렌즈 발산 **30건** → 항목별 적대적 실현가능성 검증(코드·DB 실측 동반, **6건 REFUTED 탈락**) →
> 생존 24건을 심사 3인(운영자·기술리드·제품책임자)이 0~10 채점.
> 기준 HEAD: 8b5b3f5 (fix/2026-07-11-review-remediation). 작성일 2026-07-25.
이 시스템에서 "AI"라고 부를 수 있는 것은 사실상 하나뿐이다 — `analysis/llm_assistance.py`의 Ollama `qwen2.5:0.5b-instruct` 요약이고, 그것마저 `risk_scoring.py`가 `EvidenceSource.LLM_SUMMARY`를 명시적으로 0점 처리해 판정에 기여하지 않는다. 나머지 지능은 전부 손으로 쓴 규칙이다: 검색 쿼리 생성 321줄 템플릿(`analysis/search_query_generation.py`), 위험 점수 174줄 정수 가중치(`analysis/risk_scoring.py`), 2001년식 Haar cascade 얼굴 검출(`analysis/face_person_detection.py`). 이 문서는 "더 큰 모델을 붙이자"가 아니라 **지금 이미 켜져 있는 AI가 조용히 고장나 있는 지점을 먼저 고치고**(프롬프트가 증거의 90%를 잘라먹는 문제, 시드 경로가 쓰기 락을 잡은 채 LLM을 도는 문제), 그 위에 의존성 0짜리 낭독형 신호(EXIF/IPTC 권리 필드, YuNet 검출 신뢰도)를 얹은 뒤, 판정 라벨이 실제로 쌓인 다음에야 학습·자동화로 간다는 3단계 순서를 제안한다. 심사 3인은 각각 다른 렌즈(운영자·기술리드·제품책임자)에서 출발했지만 **1순위가 만장일치로 같았다** — 「LLM 예산·계측·입력 경계」(평균 7.90, 3인 중 최고점 단독 1위).
**이 심사가 준 가장 중요한 신호**: "새 AI 모델을 붙이는" 계열은 전부 하위권으로 내려갔고(의미지문 인덱스 2.70, 판정예측 트리아지 2.30, 워터마크 검출기 4.50), 상위권은 **이미 있는 AI를 정직하게 만드는 것**과 **모델 없이 얻는 신호**(EXIF 권리필드 7.23, YuNet 7.30, 방송캡처 포렌식 6.53)가 차지했다. "AI를 적극 활용한다"의 이 코드베이스에서의 실제 번역은 *더 큰 모델*이 아니라 **① 지금 켜져 있는 LLM이 근거를 실제로 읽게 만들고 ② 모델 없이 얻을 수 있는 신호를 먼저 전부 줍고 ③ 판정 라벨이 쌓인 뒤에 학습을 켠다**는 순서다.
## 현재 AI 표면 진단
| 축 | 현재 구현 | 실제 성격 | 사각지대 |
|---|---|---|---|
| 생성형 LLM | `analysis/llm_assistance.py:22` `OllamaGenerateLlmClient` (qwen2.5:0.5b, temp 0.1, timeout 30s) | 요약 1종. `risk_scoring.py`에서 0점 | `options`에 `num_ctx` 없음 → 증거 대부분이 모델에 도달 전 절단. 저장된 llm 증거 9행에 **`summary` 키 자체가 없다** — 요약 본문이 영속화되지 않아 운영자에게 한 번도 보인 적이 없다 ※ |
> ※ **이 한 줄은 미커밋 작업 트리에서 이미 고쳐지고 있다.** `store_serialization.py`의 `_evidence_payload`에 `summary`/`sourceUrls`가 추가돼 있다(현재 `git diff`에 존재). DB의 9행이 비어 있는 것은 2026-05-26에 생성된 **과거 데이터**다. 다만 이 수정은 문제의 방향을 바꾼다 — 요약이 **드디어 화면에 보이기 시작하므로**, 잘린 증거로 만든 요약의 품질 결함이 이제 운영자 눈앞에 노출된다. 1위 항목(입력 경계)의 시급성은 이 수정으로 **낮아지는 게 아니라 높아진다.**
| 프롬프트 | `llm_assistance.py:133 _evidence_prompt` | `data={item.data}` 딕셔너리 통째 f-string | 제출건당 13,733~62,291자(최대 63행). 근거 링크는 잘리지 않은 전체 목록에서 뽑혀 붙음 |
| 위험 점수 | `analysis/risk_scoring.py` 174줄 | 손튜닝 정수 가중치 + 밴드 70/30 하드코딩 | 학습·캘리브레이션 0. 실측 밴드 분포 high 4 / medium 2 / low 0 — 트리아지 변별력 붕괴 |
| 쿼리 생성 | `analysis/search_query_generation.py` 321줄 | 한/영 템플릿 규칙 + generic 블록리스트 | `plan()`의 `priority`가 API 표면에 안 나옴. `_is_generic_local_query` 필터 결과가 GUI 추천에 반영 안 됨 |
| 로컬 CV(얼굴) | `analysis/face_person_detection.py:_detect_with_opencv` | Haar frontal + profile 2종 | `person_count = face_count` — 사람 축이 사실상 부재. confidence는 `internal_analyzer.py`에서 0.8 하드코딩. 제출 1건당 detect가 **2회** 호출됨(`internal_analyzer.py:78`, `store_enrichment.py:184`) |
| 로컬 CV(지문) | `analysis/fingerprints.py:_perceptual_hash` | 이름은 pHash, 실제는 8x8 average hash | 크롭 한 번에 무력화. 실측: 무관한 제출 이미지 쌍이 이미 0.25~0.781을 냄 |
| 텍스트/로고 | 없음 | — | 이미지 표면에 저작권자가 직접 써 놓은 워터마크·ⓒ를 읽는 경로가 0줄 |
| 원본 메타데이터 | `analysis/preprocessing.py:169-171` | "raw pixel 재구성으로 EXIF/GPS/ICC 전량 파기" | 파기 **전에 읽는 코드가 없음**. `getexif`/`getiptcinfo` src 전역 0건 |
| 사람 라벨 활용 | `risk_scoring.py:15` 하드 스킵 + export 컬럼 1개 | operator_status를 점수에서 빼는 용도만 | 통계로 되먹이는 코드 0줄 |
| 계측·재현성 | 없음 | — | model/prompt/latency 필드가 증거 payload 40여 개 중 0개. `COPYRIGHTER_LLM_DAILY_LIMIT`은 화면 표시용이고 아무것도 막지 않음 |
| 죽은 코드 | `analysis/evidence_enrichment.py:EvidenceEnricher`, `jobs/review_enrichment_job.py` | 테스트에서만 인스턴스화 | 실제 LLM 호출 지점은 `server/store_operations.py:471 _ensure_llm_summary` 한 곳 — 발산 단계 아이디어 3건이 이 죽은 코드를 수정 대상으로 오지목했다 |
**검증 단계에서 확정된 데이터 실측치** (이 문서의 모든 라벨 의존 판단의 근거):
- `submissions` 6건 — **전부 `decision_status='unreviewed'`**. `'Operator decision created'` 감사 이벤트 **0건**.
- `evidence` 292행 — **`operatorStatus` 전부 `None`**. 백업 DB에 `false_positive` 2행이 전 이력.
- `queryStrategy` 유의미 값 0행(None 288 / '' 4). `entity` 0행, `promoted` 0행, `domainSource` 전부 None.
- 즉 **"사람이 만든 라벨과 근거 페어가 이미 축적 중"이라는 전제는 이 레포에서 확인되지 않는다.**
## 제약 조건
1. **폐쇄망 번들** — 런타임 서드파티는 `numpy` / `opencv-python-headless` / `pillow` 3개뿐이고 `requirements.txt` 헤더가 그것을 명시 선언한다. 4번째 의존성 추가(onnxruntime 40~55MB, defusedxml 25KB)는 문서 한 줄 수정이 아니라 **반입 심사 안건**이다. 모델 자산은 별도: 3B Q4_K_M +약 1.9GB, 7B +4.7GB, OCR ONNX 56~72MB(제안된 "12MB"는 약 5배 과소), 롤백 태그 유지 시 +379MiB.
2. **CPU 지연** — 발산 단계 6개 아이디어가 전부 **프리필(prompt eval)을 무시**했다. 생성 속도만 대역폭으로 추정했으나 프리필은 연산 바운드다. 실측: 현행 프롬프트·증거 10건에서 20스레드 5.9초 / 2스레드 20.6초, 증거 20건이면 28.2초 / 72.7초로 `timeout=30`을 확정적으로 초과. 3B Q4는 압축 프롬프트에서도 60~180초. **7B는 현재 근거로 인터랙티브 불가.**
3. **거버넌스** — (a) `governance/policies.py:86 assert_no_biometric_template`은 top-level 키 3개만 보는 얕은 검사지만, 하드 바운더리는 코드가 아니라 사양 문서 두 곳(`docs/operations/image-rights-risk-filter.md:16`, `docs/project-introduction-and-technical-implementation.md:209`)의 "Do not store face embeddings ... or biometric templates" 문구다. 전용 테이블 우회는 코드는 통과해도 사양은 통과하지 못한다. (b) `DataClass.LLM_SUMMARY`의 근거링크 강제는 **현재 형식적으로만 통과 중** — 링크는 전체 증거에서, 요약은 잘린 증거에서 나온다. (c) 신청자 노출 경계: `memo`가 `APPLICANT_EXPORT_FIELDS`에 포함되므로 LLM 출력이 `decision_memo_presets`에 착지하면 신청자에게 그대로 나간다.
4. **판정 주체는 사람** — 프롬프트 계약(`llm_assistance.py:43-49`)에 명시. 개별 판정뿐 아니라 **판정 규칙 생성**도 이 계약에 걸린다(규칙은 판정의 집합 형태).
5. **배관 비용** — 새 `EvidenceSource` 1종을 추가하려면 `records.py` / `_ui_source` / `_ui_group` / `_evidence_payload` / `_domain_evidence_from_ui` / `_domain_source_from_ui` / `risk_scoring` 분기 / `app.js` 3곳 / `store_operations.py:488` LLM 입력 필터 / `operator-labels.js` — **12곳을 한 커밋에** 고쳐야 하고, 라운드트립 점수 보존 테스트가 선행돼야 한다. `_ui_source`의 폴백이 `"fingerprint"`라 누락 시 조용히 오분류된다.
6. **인프라 부재** — `src` 전역에 백그라운드 스레드 0개, 잡 큐·상태 테이블·폴링 엔드포인트 0개. 463개 테스트 중 스레드를 다루는 것 0개. SQLite는 WAL이 아니다(`store_persistence.py:19`). `rerun_enrichment`는 명시적으로 동기다. **"비동기 경로에 태운다"는 경로 변경이 아니라 없는 서브시스템을 만드는 것이다.**
*(정정: 발산·검증 단계에서 "SQLite가 WAL이 아니다"라는 서술이 나왔으나 **사실이 아니다.** `sqlite_store.py:162 initialize()`가 `pragma journal_mode = wal`을 설정하고 이 설정은 DB 파일에 영속되므로 `_connect()`가 매번 다시 걸 필요가 없다 — 실제 `data/copyrighter.sqlite3`도 `wal`을 반환한다. 워커 쓰기가 HTTP 읽기를 블록하는 위험은 **이미 해소돼 있다.**)*
## 후보 전체 (심사 점수순)
심사 3인(운영자 / 기술리드 / 제품책임자)이 각각 독립으로 0~10 채점한 평균. 점수 인플레 금지 조건을 걸어 전체 평균은 4.7이다.
| 순위 | 아이디어 | 렌즈 | 평균 | 운영자 | 기술 | 제품 | 검증 verdict | 공수(원안→수정후) | 오늘 착수 |
|---:|---|---|---:|---:|---:|---:|---|---|---|
| 1 | **LLM 예산·계측·입력 경계** | 비용·쿼터·감사 | **7.90** | 7.5 | 8.2 | 8.0 | **FEASIBLE** | M (입력경계 단독 S) | 가능 |
| 2 | **YuNet 초상권 정밀화** | 로컬 비전 | **7.30** | 7.0 | 7.4 | 7.5 | FWC | M | 가능 |
| 3 | **원본 메타 권리추출** | 로컬 비전 | **7.23** | 7.2 | 7.0 | 7.5 | FWC | S→M (EXIF+IPTC만 S) | 가능 |
| 4 | 방송캡처 포렌식 | 로컬 비전 | 6.53 | 6.8 | 6.8 | 6.0 | FWC | S→M | 가능(0점 라벨) |
| 5 | 모델 번들 무결성 검증 | 폐쇄망 번들링 | 6.10 | 5.0 | 6.8 | 6.5 | FWC | M | 가능 |
| 6 | AI 선연산 잡 큐 | 지연·배치 | 6.07 | 6.2 | 6.0 | 6.0 | FWC | M | 부분(락 이동만) |
| 7 | 근거공백 액션 플래너 | 자동 다음 액션 | 6.00 | 5.8 | 6.2 | 6.0 | FWC | M→**S** | 가능 |
| 8 | 작업별 모델 라우터 | LLM 파이프라인 | 5.73 | 5.5 | 6.2 | 5.5 | FWC | M→L (1단계 S) | 부분 |
| 9 | 판정 골든셋 회귀게이트 | 사람 판정 학습 | 5.63 | 4.5 | 6.4 | 6.0 | FWC | M | 부분(각인만) |
| 10 | 모델·프롬프트 버전 스탬프 | 재현성·감사 | 5.37 | 3.5 | 6.6 | 6.0 | **FEASIBLE** | S | 가능 |
| 11 | 증거 영역 오버레이 | 증거 하이라이트 | 5.33 | 5.0 | 6.0 | 5.0 | FWC | M→L | 좌표계 수정 선행 |
| 12 | 골든 케이스 회귀 하네스 | 오프라인 평가 | 5.03 | 3.8 | 5.8 | 5.5 | FWC | M→**S** | 가능 |
| 13 | 요약 상태 배지(5단→3단) | AI 실패 UX | 4.90 | 4.8 | 5.4 | 4.5 | FWC | M | 가능 |
| 14 | 인용형 증거팩 요약 | LLM 파이프라인 | 4.50 | 4.2 | 4.8 | 4.5 | FWC | M→L | 1위 선행 필수 |
| 15 | 워터마크·ⓒ 검출기 | 로컬 비전 | 4.50 | 4.8 | **3.2** | **5.5** | FWC | L→**XL** | 모델 반입 심사 |
| 16 | 밴드 임계값 스윕·섀도 리스코어 | 사람 판정 학습 | 4.30 | 2.8 | 4.6 | 5.5 | **검증 미수행** | M(원안) | 라벨 대기 |
| 17 | 페이지 권리표기 추출 | LLM 파이프라인 | 3.87 | 4.0 | 3.6 | 4.0 | FWC | L→**XL** | 페치 재설계 선행 |
| 18 | 근거 신뢰도 재보정 | 사람 판정 학습 | 3.57 | 3.2 | 4.0 | 3.5 | FWC | S | 가능(n=0 안전) |
| 19 | 문장별 근거 인용 검증기 | 그라운딩 검증 | 3.03 | 2.3 | 3.8 | 3.0 | FWC | M→L | 1위 선행 필수 |
| 20 | 근거한정 케이스 Q&A | LLM 파이프라인 | 2.97 | 2.5 | 3.4 | 3.0 | FWC | L | 라우터·캐시 선행 |
| 21 | AI 수용률 계기판 | AI 신뢰도 측정 | 2.87 | 2.2 | 3.4 | 3.0 | FWC | M→**S** | 라벨 대기 |
| 22 | 의미지문 인덱스 | 로컬 비전 | 2.70 | 3.0 | 2.6 | 2.5 | FWC | L→**XL** | **거버넌스 승인 필요** |
| 23 | 판정예측 트리아지 레인 | 트리아지 | 2.30 | 2.7 | 2.2 | 2.0 | FWC | L→**XL** | 라벨 대기 |
| 24 | 근거프로파일 일관성 감시 | 사람 판정 학습 | 2.27 | 2.0 | 2.8 | 2.0 | FWC | L | 라벨 대기 |
FWC = FEASIBLE_WITH_CHANGES(수정 조건부 실현가능). 24건 중 verdict가 무조건 FEASIBLE인 것은 1위와 10위 둘뿐이다. 16위는 검증 단계에서 판정이 기록되지 않았다 — **미검증 상태**로 표기하며, 착수 전 별도 검증이 필요하다.
**심사자 의견이 갈린 항목**(참고용 — 착수 결정 시 이 갈림이 곧 리스크다):
- **15위 워터마크·ⓒ 검출기** — 제품 5.5 vs 기술 **3.2**. 제품 렌즈는 "이미지 표면의 저작권 표기를 못 읽는 것이 가장 큰 탐지 공백"으로 보고, 기술 렌즈는 실측 템플릿 매칭 2,347ms/장과 로고 수에 선형인 비용을 근거로 반대했다.
- **10위 모델·프롬프트 버전 스탬프** — 기술 6.6 vs 운영자 **3.5**. 운영자에게는 화면에 글씨 하나 느는 일이고, 기술 렌즈에는 이후 모든 모델 변경 판단의 전제다.
- **12위 골든 케이스 회귀 하네스 / 9위 판정 골든셋 회귀게이트** — 두 항목 모두 기술·제품 대비 운영자 점수가 1.5~2.0점 낮다. 품질 기반 작업의 전형적 패턴으로, 운영자 체감이 0이라는 뜻이지 가치가 없다는 뜻은 아니다.
## 상위 후보 상세
> 배열 순서는 **점수순이 아니라 착수 순서**(선행조건 의존 관계)다. 각 제목에 심사 순위와 평균을 함께 표기했다.
> 점수 상위권 중 이 절에 상세가 없는 3건(4위 방송캡처 포렌식, 5위 모델 번들 무결성 검증, 8위 작업별 모델 라우터)은
> 바로 아래 「상위권 중 상세 미수록 3건」에 요약했고, 로드맵 2단계에 항목으로 들어가 있다.
### 1. LLM 예산·계측·입력 경계 (**심사 1위 · 평균 7.90 · 1순위 만장일치 3/3**, 공수 M → 입력경계 단독 S)
**무엇을** — 세 가지 묶음이지만 **입력 경계만 떼어 먼저 낸다**(검증자 권고). (a) `_evidence_prompt`가 `data` 딕셔너리를 통째로 f-string에 박는 것을 화이트리스트(`reason`/`match`/`similarity`/`page_title`/`domain`/`query`)로 교체, (b) 증거 상한(위험 기여 상위 12건), (c) `options`에 `num_ctx` 명시. 이어서 별건으로 예산 집행(LlmCallPolicy)과 계측(p95 링버퍼).
**왜** — 지연 감소가 아니라 **거버넌스 결함 수정**이다. 프롬프트는 잘리는데 `source_urls`/`source_evidence_ids`는 잘리지 않은 전체 증거에서 뽑혀 붙는다. 결과적으로 `assert_operator_evidence_payload_allowed(LLM_SUMMARY)`는 통과하지만 화면의 "연결 근거 N건"에서 N은 거짓이다. 모델이 본 적 없는 근거를 근거라고 표시하는 상태이며, 다른 23건 중 어느 것도 이 구멍을 닫지 않는다.
**AI 구성요소·모델·지연** — 모델 변경 없음(qwen2.5:0.5b 유지). 추론 추가 0. **24건 중 유일하게 지연을 줄인다**: 실측 20.6초 → 1.5~8.3초(중앙값 2~3초). 부수 효과로 축약 프롬프트에서 모델이 한국어 2단('요약:'/'확인 필요:') 형식을 지키기 시작했다(현행 프롬프트에서는 형식·언어 계약을 전부 무시하고 영어로 답했다).
**코드 근거** — `llm_assistance.py:133 _evidence_prompt`(data 통째 덤프) · `llm_assistance.py:52-53`(options에 temperature만) · `env_clients.py:57`(`COPYRIGHTER_LLM_DAILY_LIMIT`이 파싱만 되고 naver/google과 달리 `daily_limit=`으로 배선되지 않음) · `store_persistence.py:231-246 _apply_provider_usage_delta`(usage 누적만, 상한 대조 없음) · `store_operations.py:471 _ensure_llm_summary`(실제 유일한 호출 지점).
**폐쇄망 번들** — 증분 0바이트, 신규 의존성 0, 스키마 마이그레이션 0, 새 EvidenceSource 0. 텔레메트리 송출 없음(전부 로컬 SQLite).
**가드레일** — `face_crop_search`/`crop_index`/`privacy_note`/`thumbnail_url`을 모델 입력에서 제외해 초상 경계를 프롬프트 레벨에서도 지킨다(`assert_no_biometric_template`과 정합). 쿼터 소진은 오류가 아니라 강등(`SEARCH_SKIPPED` 처리 방식과 동일).
**검증자가 지적한 조건** — (1) 세 가지를 한 커밋에 묶지 말 것. (2) 화이트리스트만으로 부족 — 축약해도 56건이면 3,000토큰대라 **증거 상한이 필수**이고, `num_ctx`만 올리고 상한을 안 두면 2스레드 기준 프리필에만 약 17초가 들어 **순증가**한다. (3) `promotion_reason`은 화이트리스트에서 뺀다 — 모델이 그대로 베껴 요약이 증거 목록 복붙이 된다(실측 출력이 정확히 그랬다). (4) 예산 카운터의 진실원은 `ExternalApiPolicy` 복제가 아니라 SQLite provider payload로 — 복제하면 프로세스 메모리 카운터라 재시작 시 0으로 리셋돼 화면 usage와 집행 기준이 갈라진다. (5) p95 표본은 최근 200건 링버퍼로 상한 명시.
**함께 넣을 것** — 심사자 3인이 공통 지목한 5줄 수술: `_seed_from_image_store_locked`(`sqlite_store.py:310`) 안의 `_ensure_llm_summaries_for_existing_source_evidence` 호출을 `_write_lock` **밖으로** 이동. `record_decision`도 같은 락을 쓰므로, 지금은 폴더 재적재 중에 운영자가 판정 버튼을 눌러도 락 대기에 걸린다. `store_operations.py:457-458`이 이미 "락 밖에서 LLM 호출" 선례를 갖고 있다.
### 2. 원본 메타 권리추출 (**심사 3위 · 평균 7.23**, 공수 S→M / EXIF+IPTC만이면 S)
**무엇을** — 외부 전송용 파생본을 만들며 메타데이터를 파기하기 **전에**, 원본 바이트에서 권리 필드만 화이트리스트로 읽어 증거로 만든다. EXIF `0x8298 Copyright` / `0x013B Artist` + IPTC `CopyrightNotice`(2:116)/`Credit`(2:110)/`Source`(2:115).
**왜** — 저작권자가 파일 안에 직접 써 넣은 정답을 지금 100% 버리고 있다. 추론이 아니라 낭독이라 오탐 가능성이 사실상 없고, 히트하면 외부 API 왕복(수 초 + 쿼터) 없이 판정 근거가 선다. 미스하면 섹션이 아예 안 뜬다 — **리스크 구조가 이 목록에서 가장 깨끗하다**(운영자 심사자 평).
**AI 구성요소·모델·지연** — AI 모델 없음(컨테이너 파싱). 실측 0.1ms 미만/장. 픽셀 기반 AI-생성 판별기는 의도적으로 배제 — CPU/폐쇄망에서 도메인 시프트에 취약하고 반증 가능성이 없다.
**코드 근거** — `preprocessing.py:169-171 _upright_derivative`("Rebuild from raw pixels so NO metadata survives") · `preprocessing.py:build_external_derivative` · `image_store.py:60-68`(metadata가 submission_id/title/file/format 4개뿐) · `governance/policies.py:82 policy_for`(미등록 DataClass는 KeyError → 신규 `RIGHTS_METADATA` 등록 강제).
**폐쇄망 번들** — EXIF+IPTC는 pillow 12.2.0에 이미 포함, 증분 0. **단 XMP는 폐쇄망에서 조용히 죽는다**: Pillow의 `Image.getxmp()`는 `defusedxml`이 없으면 경고와 함께 빈 dict를 반환하는데(직접 차단 실측), 이 개발 머신의 defusedxml은 anaconda/nbconvert/Scrapy가 끌고 온 것이고 `requirements.txt`에는 없다. **납품 타깃에서 XMP `dc:rights`·`digitalSourceType`·C2PA 선언 탐지는 전부 no-op**이 된다.
**가드레일** — GPS·카메라 시리얼·소유자명·XMP-mwg-rs Face regions는 읽지도 저장하지도 않는다(화이트리스트를 코드 상수로 두고 테스트로 고정). 메타데이터는 위조 가능하므로 "자기신고 정보"로 라벨링하고 자동 반려하지 않는다.
**검증자가 지적한 조건** — (1) XMP/C2PA는 `defusedxml` 반입 안건으로 **분리**하고, 추가 전까지는 "이 배포에서는 XMP 미판독"을 기동 로그에 명시. (2) **스톡 자산 ID +70(단독 high)은 논리적으로 틀렸다** — Getty/Adobe 자산 ID는 정품 라이선스 구매본에도 그대로 박혀 배포된다. 올바른 귀결은 반려가 아니라 "라이선스 증빙 요구"이므로 +30(medium)으로 낮춘다. (3) recall이 낮다 — 카카오톡/인스타/트위터를 거친 이미지는 메타가 이미 제거돼 있어 히트율이 한 자릿수~십몇 %일 가능성. 따라서 **GUI 섹션은 추출값이 하나라도 있을 때만 렌더**(빈 섹션 금지). (4) 추출 훅은 `InternalAnalyzer.analyze` 진입부(원본 `ImagePayload.content`가 살아 있는 지점)에 둔다 — 파이프라인 순서 변경 불필요. (5) `.svg`가 `SUPPORTED_IMAGE_SUFFIXES`에 있으나 Pillow가 못 여니 추출 실패를 예외가 아니라 "메타 없음"으로 degrade.
### 3. YuNet 초상권 정밀화 (**심사 2위 · 평균 7.30**, 공수 M)
**무엇을** — Haar cascade를 `cv2.FaceDetectorYN`(YuNet)로 교체하고 검출 신뢰도를 실제 값으로 채운다. 얼굴 면적 비율 + 검출 수로 "식별 가능한 근접 인물"과 "배경 군중"을 등급 분리. `FacePersonSignal` 인터페이스와 `face_boxes` 계약은 유지해 크롭·얼굴 웹탐지 경로 무변경.
**왜** — 두 방향 동시 이득. 놓침: 뒷모습·마스크·군중·저조도 인물이 지금 전부 신호 0. 낭비: Haar 오탐 박스가 `+35`와 face_crop 파일을 소비한다. 그리고 **밴드 분포가 이미 붕괴했다**(실측 high 4 / medium 2 / low 0) — `FACE_PERSON +35`가 사람 있는 사진 전부에 붙어 medium 문턱을 혼자 넘기기 때문이다. 등급화는 트리아지 변별력을 직접 회복시킨다.
**AI 구성요소·모델·지연** — YuNet ONNX 약 230~345KB, CPU 640x480 약 10ms. **현재보다 빨라진다**: 실측 Haar 2캐스케이드 1920x1080 = 218ms/호출 × 건당 2회 = 약 440ms → YuNet 약 20ms. **순감 약 0.4초/장.**
**코드 근거** — `face_person_detection.py:_detect_with_opencv`(scaleFactor 1.1 / minNeighbors 4 고정) · `face_person_detection.py:103`(`person_count = face_count`) · `face_person_detection.py:57-67`(OpenCV 5.x가 CascadeClassifier를 제거한다는 경고가 이미 코드에 있음 — 이 교체가 그 미래 호환 문제까지 해소) · `internal_analyzer.py:78`과 `store_enrichment.py:184`(detect 중복 호출) · `internal_analyzer.py`(confidence 0.8 하드코딩).
**폐쇄망 번들** — 파이썬 의존성 0(설치본 opencv-python-headless 4.13.0에 `cv2.FaceDetectorYN` 존재를 직접 확인). 모델 자산 345KB로 사실상 무시 가능.
**가드레일** — YuNet이 반환하는 5점 랜드마크는 **읽는 즉시 폐기**하고 저장하지 않는다. 나아가 `assert_no_biometric_template` 금지 키에 `landmarks`/`face_landmarks`/`face_descriptor` 추가(호출부가 2곳뿐이라 기존 데이터 로드 무영향 — 정책 강화 방향, 무충돌).
**검증자가 지적한 조건** — (1) **사람 검출기(YOLOv8n/MobileNet-SSD)는 별건으로 분리**. cv2.dnn으로 YOLOv8 ONNX를 쓰려면 NMS를 직접 구현해야 하고 저사양 CPU에서 0.3~1.5s/장이다. YuNet 345KB/10ms와 같은 묶음으로 취급하면 안 된다. (2) 같은 커밋에서 `detect()` **중복 호출을 제거**한다. (3) `tier`(등급 라벨)와 실제 신뢰도를 `_evidence_payload`와 `_domain_evidence_from_ui` **양쪽**에 추가하고 라운드트립 점수 보존 테스트를 먼저 작성 — 안 하면 재채점 때마다 전 건이 +35로 복귀한다. (4) 등급화는 기존 medium 다수를 low로 이동시켜 `decision_stats`의 캘리브레이션 기준선을 리셋한다. **판정 0건인 지금이 이 비용을 치르기 가장 싼 시점**이다(나중이면 판정 이력과 기준선이 어긋난다).
### 4. 판정 골든셋 회귀게이트 (심사 9위 · 평균 5.63 — 운영자 4.5 vs 기술 6.4, 공수 M / **각인 부분만 오늘 즉시**)
**무엇을** — 원안은 결정 케이스를 evidence 스냅샷과 함께 내보내 verify에서 재채점하는 하네스. 검증 결과 **원안의 절반은 오늘 성립하지 않고, 나머지 절반은 오늘 당장 실효가 있다.**
**왜 (오늘 유효한 절반)** — 검증자가 24건 전체에서 "가장 실효가 큰 한 줄짜리 지적"으로 꼽은 것: `_rescore_submission`이 12곳에서 `riskScore`를 덮어쓰므로 **저장된 점수는 이미 왕복 점수**다. 그것을 다시 왕복 채점과 비교하면 항상 일치한다. 즉 각인 없이 만든 점수 재현 테스트는 **항상 통과하는 빈 테스트**다. 막으려는 실제 버그(`search_image` 왕복 뒤집힘, `risk_scoring.py:105-112` 주석)는 원본 채점 vs 왕복 채점의 차이인데 원본 점수가 어디에도 저장되지 않는다.
**AI 구성요소·모델·지연** — 모델 없음(평가 하네스). verify 단계에서만 도는 오프라인 하네스라 운영자 체감 0. 실측 1000건 전량 재채점 0.3초 미만.
**코드 근거** — `store_search_candidates.py:721-731 _rescore_submission` 및 호출 12곳(`sqlite_store.py:309,406,743` / `store_enrichment.py:125,167,298,358` / `store_operations.py:324,385,462` / `store_search_candidates.py:160`) · `store_serialization.py:124`(`fileFacts.analysis`가 리터럴 'v1', `AnalysisRun.analysis_version`은 submission payload에 각인되지 않음) · `store_serialization.py:271-275`(라운드트립 불변식이 주석으로만 존재).
**폐쇄망 번들** — 신규 의존성 0. **비용은 번들이 아니라 반출 리스크**: 실측 evidence 평균 632바이트 × 케이스당 약 49행 = 약 31KB/건, 1000건이면 약 31MB짜리 파일이 result URL·pageTitle·thumbnailUrl·내부 riskScore를 통째로 담는다.
**가드레일** — 골든셋은 `APPLICANT_EXPORT_FIELDS` 경로와 **물리적으로 다른 모듈**에 두고, `.gitignore` + 납품 번들 생성 스크립트 제외 목록 + `offline-install.md`에 3중 명시.
**검증자가 지적한 조건** — (1) **선행 필수**: `initialRiskScore` / `initialRiskBand` / `scorerVersion`을 `_submission_payload`(`store_serialization.py:106`)에 추가하고 `_rescore_submission`이 절대 덮어쓰지 않게 한다. (2) 골든셋 생성에 `export_decisions`를 재사용하지 말 것 — 호출마다 'Decisions exported' 감사이벤트를 남기고 `audit_events()`는 최신 500행으로 잘리므로 verify가 돌 때마다 운영자 감사 화면이 노이즈로 밀려난다. (3) 지표 회귀는 CI 실패가 아니라 **차이 리포트 출력만** — `highBandApprovalRate`는 코드 변경뿐 아니라 케이스 믹스 변화로도 움직인다. (4) 판정 100건 미만이면 계산하지 않는다. (5) 왕복 복원의 취약 지점(`weak_hint`가 저장되지 않고 제목 접두사 'Best guess label ' 문자열로 재구성됨, `domainSource` 미기록 레거시 행)을 스키마에 명시 필드로 굳혀 제목 문자열 의존을 끊는다. (6) **판정 0건이어도 (1)+(5)만으로 오늘 즉시 가치가 있다** — 라벨 없이 실제 버그를 잡을 수 있는 유일한 학습계 항목.
### 5. 모델·프롬프트 버전 스탬프 (심사 10위 · 평균 5.37 — 기술 6.6 vs 운영자 3.5, 공수 S · **verdict FEASIBLE**)
**무엇을** — 프롬프트 리터럴을 `analysis/llm_prompts.py`로 분리하고 `PROMPT_ID`/`PROMPT_VERSION` 부여. `/api/generate` 응답의 model과 `/api/tags`의 digest를 Evidence.data에 심고, `_evidence_payload`에 `modelVersion`/`promptVersion`/`latencyMs`를 추가해 GUI에 "모델 qwen2.5:0.5b · 프롬프트 ko-v2 · 1.4s"를 작은 글씨로 노출.
**왜** — 재현성 필드가 코드 전체에 **단 하나도 없다**. 모델을 올리거나 프롬프트를 고친 뒤 과거 요약과 신규 요약이 DB에 섞여도 구분이 불가능하다. 모델을 바꾸자는 모든 후속 제안이 이 스탬프 없이는 전후 비교를 못 한다. 부수 효과로 `latency_ms`가 쌓여 **운영자 대기시간 실측치가 처음으로 생긴다**.
**AI 구성요소·모델·지연** — 모델 변경 없음. 응답 메타데이터 파싱만. 추론 증가 0, `/api/tags`는 프로세스당 1회 수십 ms.
**코드 근거** — `llm_assistance.py:38-61 summarize_evidence`(모델명을 보내지만 응답의 model을 `_response_text:143-154`가 버림) · `llm_assistance.py:41-50`(버전 없는 시스템 프롬프트 리터럴) · `store_serialization.py:256-322 _evidence_payload`(40여 필드 중 모델/프롬프트 0개) · `store_serialization.py:428-430 _evidence_id`.
**폐쇄망 번들** — 증분 0. 프롬프트 파일도 코드 동봉.
**가드레일** — 스탬프는 메타데이터일 뿐 판정 무영향(LLM_SUMMARY 0점 유지). 모델명은 admin 전용 `PROVIDER_METADATA` 정책과 정합하게 노출.
**검증자가 지적한 조건** — (1) **id 해시 오염 주의**: `_evidence_id`는 data 전체의 sha256이라 `latency_ms`/`generated_at`을 data에 넣으면 버전이 바뀔 때가 아니라 **매 호출마다** id가 바뀐다. `model`/`model_digest`/`prompt_version`만 data에, 시간 관련 필드는 `_evidence_payload` 전용으로 분리. (2) digest는 `/api/generate` 응답에 없다 — `/api/tags`에만 있고(실측 `a8b0c51577010a27...`, 397,821,319바이트, Q4_K_M) 그것도 blob 해시가 아니라 매니페스트 sha256이므로 "매니페스트 digest"라고 정직하게 명명. (3) 정책 필수화는 신규 생성 경로에만 적용하고 legacy 행은 `prompt_version='legacy'`로 1회 마이그레이션. (4) 반환 타입을 dataclass로 바꾸면 `FakeInternalLlmClient`와 기존 테스트 3건이 함께 바뀐다.
### 6. 근거공백 액션 플래너 (심사 7위 · 평균 6.00, 공수 M→**S**)
**무엇을** — 원안은 신규 엔드포인트 + LLM 재랭킹이었으나 **검증 결과 둘 다 제거**되고, 서버 `plan()` 결과를 `(query, strategy, source, priority)`로 기존 `review()` 응답에 실어 보내고 `_is_generic_local_query`가 걸러낸 쿼리를 화면에서 숨기는 것으로 축소됐다.
**왜** — 파일명이 `IMG_2831`인 케이스에 GUI가 추천하는 쿼리는 `IMG_2831 저작권`이다. 실행 버튼이 그대로 노출되어 운영자가 헛검색을 돌리고 외부 쿼터를 태운다. `_is_generic_local_query`의 정규식 `(img|image|photo|picture|portrait|screenshot|download|copy|scan|file)[\s_-]*\d*`가 이 계열을 정확히 잡는데, 그 필터가 서버에만 있고 GUI 추천에는 반영되지 않는다.
**AI 구성요소·모델·지연** — **LLM 없음**(검증자 권고로 재랭킹 제거). `plan()`은 순수 정규식으로 μs 단위, `review()` 응답에 얹으면 체감 0.
**코드 근거** — `search_query_generation.py:19 plan()` · `search_query_generation.py:295 _is_generic_local_query` · `evidence-guidance.js:suggestedEvidenceQueries`(서버와 무관하게 seed+['저작권','공식','이미지 출처'] 템플릿) · `app.js:1083 renderEvidenceNextActions` · `store_serialization.py:285`(`query_priority`가 data엔 들어가지만 `_evidence_payload`가 매핑 안 함).
**폐쇄망 번들** — 증분 0.
**가드레일** — 추천 쿼리 실행은 여전히 운영자 클릭이 필요하고 `SearchApiPolicy` 쿼터를 그대로 통과.
**검증자가 지적한 조건** — (1) 원안의 "`plan()`의 strategy가 API 표면에 나온 적 없다"는 **사실이 아니다** — `store_enrichment.py:447,620`이 이미 `plan()`을 호출하고 `queryStrategy`로 직렬화해 `app.js:1886`이 화면에 그린다. 실제로 빠진 것은 `priority` 하나뿐이다. (2) 인용된 `evidence_enrichment.py:58 enrich_latest`는 **죽은 코드**다. (3) `knowledge_entries`를 `plan()`에 넘기면 오히려 나빠진다 — 관련성 검사 없이 활성 엔트리 전부에 대해 priority 70/68 후보를 만들어 로컬 메타(54)를 밀어낸다. 프로덕션 두 호출부가 모두 `[]`를 넘기는 것은 우연이 아니다. (4) LLM 재랭킹은 후보 총량이 애초에 3~5개라 정보 이득이 사실상 0인 전형적 과설계. (5) 폴백 전제가 거짓 — `env_clients._llm_assistant()`는 도달성 검사 없이 무조건 어시스턴트를 반환하므로 `llm_assistant is None` 분기는 **절대 실행되지 않는다**.
### 7. 골든 케이스 회귀 하네스 (심사 12위 · 평균 5.03 — 운영자 3.8, 공수 M→**S**)
**무엇을** — 원안 3축 중 **축(2) 라운드트립만** 남기되 케이스 고정이 아니라 **속성 기반 테스트**로 만든다: `match`/`promoted`/`weak_hint`/`domainSource`/`knowledge_entry_type`/`similarity` 조합을 열거해 `_evidence_payload → _domain_evidence_from_ui → RiskScorer` 점수 보존을 전수 검사.
**왜** — `risk_scoring.py`와 `store_serialization.py` 주석이 기록한 실제 회귀 이력이 전부 이 조합에서 나왔다(FACE_PERSON 중복 +35, naver 중복 +40, search_image 왕복 0점→+45). 고정 케이스 몇 개로는 조합 폭발을 못 덮는다.
**AI 구성요소·모델·지연** — 모델 없음. 평시 추론 0초, 밀리초 단위.
**코드 근거** — `risk_scoring.py:7-99`(밴드 임계 70/30 하드코딩, 캘리브레이션 테스트 없음) · `tests/rights_filter/analysis/test_llm_assistance.py`(전체 47줄, FakeInternalLlmClient만 사용 — 실제 모델 출력 회귀 0건) · `store_serialization.py:271-275`(불변식이 주석으로만 존재).
**폐쇄망 번들** — 증분 0(pytest는 requirements-dev에 이미 있음). 픽스처는 합성 JSON 수십 KB.
**가드레일** — 픽스처는 운영 DB 익명화 추출이 **아니라 합성**한다 — 점수 기여 키만 필요하고 실제 URL·블로거명은 필요 없다. 원안의 "이미지가 안 들어가니 안전"은 틀렸다(evidence payload에 `bloggerName`/`bloggerLink`/`pageTitle`/`url`이 그대로 들어간다).
**검증자가 지적한 조건** — (1) 축(1) 밴드 정확도는 **정답 라벨의 출처가 없다** — 사람이 기대 밴드를 손으로 적으면 "손튜닝 가중치를 손으로 만든 기대값에 맞추는" 동어반복. "정확도 임계"가 아니라 **"가중치 변경 시 전 제출건 밴드 변동 diff"**로 바꾼다. (2) 축(3) 환각률은 14번(인용 검증기)이 안정화되기 전에는 만들지 않고, 만들 때도 `test_llm_output_snapshot`으로 정직하게 명명한다(녹화 재생은 환각률 회귀가 아니라 문자열 diff이고, 실측 편차가 임계 0.1보다 크다).
### 8. AI 선연산 잡 큐 (심사 6위 · 평균 6.07, 공수 M / **락 이동 5줄만 오늘 즉시**)
**무엇을** — 원안은 `ai_jobs` 테이블 + 백그라운드 워커. 검증 결과 **"잡 큐보다 먼저 5줄로 얻을 것을 얻어라"**로 축소 권고: 시드의 LLM 순회를 `_write_lock` 밖으로 옮기고 시드 1회당 처리 건수 상한(예: 20건)을 둔다.
**왜** — 현재 `seed_from_image_store`가 `_write_lock`을 잡은 채 제출건마다 Ollama를 부르고, `record_decision`도 같은 락을 쓴다. 즉 **폴더 재적재 중에는 판정 버튼이 락 대기에 걸린다.** 실측 1건당 20.6초 기준 제출 100건이면 약 34분간 모든 쓰기가 잠긴다. 게다가 Ollama가 아예 없어도 비용이 든다 — 이 Windows 환경 실측에서 127.0.0.1:11434 연결은 즉시 refuse가 아니라 2초 타임아웃이었다(100건 × 2초 = 200초 락 점유).
**AI 구성요소·모델·지연** — 모델 변경 없음. 바뀌는 것은 추론의 위치와 시점.
**코드 근거** — `sqlite_store.py:293` `seed_from_image_store`가 `_write_lock` 획득 → `:310` LLM 순회 · `sqlite_store.py:688 record_decision`이 같은 락 · `store_operations.py:457-458`(수동 검색은 이미 "락 밖에서 호출"을 명시 — 같은 레포에 상반된 두 패턴) · `store_serialization.py:220 _provider_item_has_result`(source=='failure'에 False를 반환해 `only_if_missing`이 실패 행을 "요약 없음"으로 봄 → **매 reload마다 실패 전건 무한 재시도**) · `store_persistence.py:19 _connect()`(WAL 미설정).
**폐쇄망 번들** — 증분 0(threading + sqlite3 stdlib). 새 `ai_jobs` 테이블은 `store_schema.py` DDL + `store_constants.py:18 STORE_TABLES` 등록 필요(`store_serialization.py:480 _validate_table`이 미등록 테이블을 차단). 백업은 `Connection.backup`이 파일 전체를 뜨므로 자동 포함.
**가드레일** — 워커는 생성(HTTP) 구간을 반드시 락 밖에서 수행하고 저장 순간에만 락을 잡는다. '생성 중' 상태가 판정 버튼을 막지 않는다.
**검증자가 지적한 조건** — (1) **1번(프롬프트 축소)이 선행이다** — 축소 없이 잡 큐만 넣으면 '생성 중' 칩이 영구히 붙어 있는 화면이 된다. (2) 실패 백오프 추가(현재는 매 reload마다 전건 재시도). (3) `env_clients._llm_assistant()`에 도달성 프로브 추가 — 여러 아이디어가 "None이면 폴백"을 전제하는데 그 전제가 현재 거짓이다. (4) ~~`_connect()`에 `journal_mode=WAL` 추가~~ — **이 조건은 취소한다. 확인 결과 WAL은 이미 켜져 있다**(`sqlite_store.py:162 initialize()`, DB 파일에 영속). `store_backup.py` docstring이 WAL을 전제로 서술한 것도 정확했다. 워커 쓰기가 HTTP 읽기를 블록하는 위험은 존재하지 않는다. (5) 워커는 `run_once()`를 노출해 스레드 없이 테스트 가능하게 하고, 부팅 시 `running→pending` 리셋을 넣는다.
### 상위권 중 상세 미수록 3건 (요약)
**4위 · 방송캡처 포렌식 (평균 6.53, 공수 S→M)** — 방송 화면·앱 스크린샷을 결정적 영상처리(휘도 투영 프로파일, Canny 엣지 밀도, HSV 채도, 히스토그램 균일도)만으로 판별해 라벨을 붙인다. **ML 모델 0, 의존성 0, 실측 2ms/장.** 지금 코드에 캡처를 식별하는 줄이 하나도 없고, 오히려 `_is_generic_best_guess_label`이 'screenshot'을 블록리스트로 버린다. 심사 3인이 공통으로 "관측 가능성이 로컬비전 6건 중 최고"(오탐 원인이 사람이 읽는 규칙으로 남는다)라고 평가했다. 조건: **1차는 반드시 0점 라벨로 출시**하고 `decision_stats()` 교차표로 실제 반려와의 상관을 본 뒤 점수를 부여할 것. 레터박스·하단 텍스트밴드는 시네마틱 크롭·밈·PPT 캡처와 구분되지 않으므로 "서로 독립인 카테고리 2개 이상"을 검출 조건으로 둘 것. → 로드맵 2-f.
**5위 · 모델 번들 무결성 검증 (평균 6.10, 공수 M)** — `/api/tags` 응답 기반 `models.lock.json` + **비차단** 프리플라이트 + `enabled`와 분리된 `modelReady` 필드. 핵심 발견: `env_clients._llm_assistant()`가 **도달성 검사 없이 무조건 어시스턴트를 반환**해 llm provider가 항상 `enabled=True`이고, `offline-install.md`는 정반대로 서술한다. 즉 **provider 패널이 지금 거짓말을 하고 있다.** 이 산출식 하나를 고치면 다른 4건이 공통으로 깔고 있는 "LLM이 없으면 폴백" 거짓 전제까지 한 번에 참이 된다. 조건: 프리플라이트가 기동을 막으면 안 되고(DROP 방화벽에서 기동 정지), 운영자 긴급 비활성 토글인 `enabled`를 자동 강등이 덮어써서도 안 된다. → 로드맵 1-f + 2-b.
**8위 · 작업별 모델 라우터 (평균 5.73, 공수 M→L / 1단계 S)** — 태스크별 Ollama 파라미터 프로필과 모델 등급 분리(0.5B 분류 / 3B 주력 / 7B 옵션). 심사 3인이 공통으로 "**태스크가 문자 그대로 하나뿐인 지금 라우터는 전형적 선행 추상화**"라고 감점했고, 동시에 그 1단계(`num_ctx`/`num_predict`/`keep_alive`/`timeout`을 환경변수로 노출 + 시드 LLM 호출을 `_write_lock` 밖으로)는 "최우선급 수술"로 평가했다. **결론: 라우터가 아니라 1단계만 떼어내 쓴다**(로드맵 1-b·2-d). 7B(+4.7GB)는 1단계 실측 tok/s가 나오기 전 번들 반입 금지.
## 반증되어 제외된 것
### 통째로 REFUTED — 6건 (심사 대상에서 제외)
발산 30건 중 6건이 적대적 검증에서 전제 자체가 무너져 심사에 올라가지 못했다. **이 6건은 "나중에 하자"가 아니라 "이 형태로는 하지 말자"이다.**
| 탈락 아이디어 | 렌즈 | 무너진 전제 |
|---|---|---|
| **LLM 쿼리 리라이터** | LLM 파이프라인 | 입력으로 쓰겠다는 지식엔트리가 파이프라인에 안 들어온다 — 프로덕션 두 호출부가 `plan(evidence, [], ...)`로 **빈 리스트**를 넘긴다(`store_enrichment.py:447,620`). 그런데 `plan()`은 이미 `entry.aliases`로 별칭 쿼리를 만든다. **별칭 확장 기계는 이미 있고 입력선만 끊겨 있다** — 한 줄 수정이 LLM보다 먼저다. 또 가드레일("입력 토큰 1개 이상 공유")과 가치(활동명↔본명·로마자 = 입력에 없는 외부 지식)가 서로를 무효화한다 |
| **반려 메모 초안기** | LLM 파이프라인 | 코퍼스가 존재하지 않는다(판정 메모 0건). 그리고 `memo`는 `APPLICANT_EXPORT_FIELDS`에 포함돼 **LLM 생성 텍스트가 신청자에게 나가는 경로를 새로 여는 유일한 아이디어**다. 반려 사유는 분쟁 시 문서 증거다. 효용도 역전 — 3B 초안 대기 40~90초 vs 수작업 타이핑 30~90초 |
| **로컬 VLM 쿼리 시드** | 로컬 비전 | "배치라 대기 경로 밖"이 거짓(시드는 `_write_lock` 안 동기 POST). 환각이 점수로 새는 경로 2개 실재 — VLM priority 58이 유료 쿼터를 잠식하고, 헛쿼리 검색 실패는 `FAILURE +30`으로 **환각이 곧바로 위험점수를 올린다**. 지연도 과소추정(이미지 프리필만 15~50초 > `timeout=30`) |
| **판정역산 가중치 학습기** | 사람 판정 학습 | n=0. 더 근본적으로 **라벨 순환이 구조적**이다 — 큐가 `riskScore` 내림차순 정렬이고 화면이 점수·근거를 먼저 보여준 뒤 판정을 받으므로, 학습된 계수는 "이 근거가 반려를 예측한다"가 아니라 "운영자가 내가 보여준 점수에 동의했다"를 재학습한다. 제안 피처 3개는 현 코퍼스에서 상수 0 컬럼 |
| **라벨가치 우선 큐** | 사람 판정 학습 | 입력인 p(반려)가 없다(위 항목이 REFUTED). 능동학습의 경제학이 성립하지 않는다 — **큐의 모든 케이스가 결국 판정되므로** 재정렬은 라벨이 언제 오는지만 바꾸고 몇 개 오는지는 못 바꾼다. 게다가 `riskScore` 내림차순은 정보 순서가 아니라 **위험 트리아지 순서**이고 미탐 비용이 비대칭이라 정당하다. 가치 있는 절반(중복그룹 우선)은 `duplicateGroup`으로 이미 클라이언트에 도착해 있다 |
| **시리즈 묶음 심사 카드** | 운영자 효율 | 0.88~0.97 밴드가 목표를 못 맞춘다(실측 반증). 실제 제출 이미지로 측정: 20% 크롭 0.812~0.859, 좌우반전 0.406~0.844로 **목표는 밴드 아래로 빠지는데**, 무관한 이미지 쌍이 이미 0.781을 낸다. 원인은 `fingerprints.py:47`이 DCT pHash가 아니라 **8x8 평균해시**라는 것. 게다가 묶음 LLM 요약은 8건 전체에서 근거 링크를 뽑으면서 실제로는 1건 남짓만 읽어 **근거 없는 요약 하나를 8건에 확산**시킨다 |
> 이 6건이 남긴 **재사용 가능한 교훈 3가지**: ① 이 레포에서 "배치니까 괜찮다"는 거짓이다 — 시드가 `_write_lock` 안에서 돈다. ② 실패 증거에 `+30`이 붙으므로 **AI가 틀리면 위험점수가 올라간다**(환각→가점 경로). ③ `_perceptual_hash`는 이름만 pHash이고 실제로는 평균해시라, 유사도 임계값에 기대는 모든 설계가 이 위에서 흔들린다.
### 살아남은 아이디어에서 잘려나간 구성요소
아래는 항목 자체는 살았지만 **내부의 특정 주장·구성요소가 반증되어 잘려나간 것들**이다. 착수 시 되살리면 안 된다.
| 잘려나간 것 | 소속 | 반증 근거 |
|---|---|---|
| **ⓒ/© OCR 매칭** | 워터마크 검출기 | OpenCV 배포 CRNN `crnn.onnx`의 알파벳은 `alphabet_36`(0-9, a-z)뿐이라 대문자·괄호·기호가 없다. 94자 `crnn_cs.onnx`에도 U+00A9가 없다. `'(C) 2024'` 정규식 매칭률 0, `stock.adobe.com`은 `stockadobecom`으로 복원 |
| **전체 프레임 로고 템플릿 매칭** | 워터마크 검출기 | 개발 머신 실측 1920x1080 · 로고 10개 × 6스케일 = **2,347ms**. KB에 로고 50개면 ×5 → 저사양 30~100초/장. 비용이 "운영자가 등록한 로고 수"에 선형이고 상한이 없다 |
| **타일 aHash 부분매칭 국소화** | 증거 영역 오버레이 | `match='partial'`은 Google Vision의 원격 URL 목록에서만 생성되며 **우리는 그 픽셀을 갖고 있지 않다**. 게다가 8x8 average hash는 실측에서 무관한 제출 이미지 쌍이 이미 0.25~0.781을 낸다 — 1/16 타일은 더 심하다. **틀린 영역을 칠하는 것은 아무것도 안 칠하는 것보다 나쁘다** |
| **LLM 정책메모 초안** | 근거프로파일 일관성 감시 | 착지점 `decision_memo_presets`의 본문이 `memo`를 거쳐 `APPLICANT_EXPORT_FIELDS`로 신청자에게 나간다. 또 "이 조합에서는 …인 경우 반려"는 시스템 프롬프트가 금지하는 판정 권고의 집합 형태 |
| **LLM 후보 재랭킹** | 근거공백 액션 플래너 | 후보 총량이 3~5개(쿼터 상한). 5개 중 3개 고르기에 0.5B + 파싱 폴백 + 범위 검증 + 배지를 붙이는 과설계. `priority`가 이미 순서를 준다 |
| **"추가 네트워크 비용 0"** | 페이지 권리표기 추출 | `page_fetcher` 호출부는 유사도 비교의 **네 번째 폴백**이라, 이미지가 실제로 걸린 페이지는 애초에 HTML을 받지 않는다. 게다가 페치 바이트는 어디에도 캐시되지 않는다 |
| **"screenshot 블록리스트는 자해"** | 방송캡처 포렌식 | 과장된 오독. `_is_generic_best_guess_label`은 검색 쿼리 적합성 판단 함수이고 'screenshot' 한 단어로 검색하는 것은 실제로 무의미하다. 차단은 옳은 설계 |
| **"수천 번 클릭한 오탐 라벨"** | 근거 신뢰도 재보정 | 실측 evidence 292행 전부 `operatorStatus=None`, 백업 DB에 `false_positive` 2행이 전 이력 |
| **"pending 프리마킹은 점수 불변이 코드로 보장"** | 근거 신뢰도 재보정 | `mark_evidence_status`(`sqlite_store.py:731-734`)가 NON_CONTRIBUTING이 아니면 `contributed`를 True로 복원하고 재채점한다. 이전에 false_positive였던 FINGERPRINT 행을 pending으로 바꾸면 +80이 되살아난다 |
| **"운영자별 편향 감지"** | 근거프로파일 일관성 감시 | `actor='rights.ops'` 상수가 문제가 아니라 **시스템에 사용자 개념 자체가 없다**(`_require_auth`는 단일 공유 bearer 토큰 하나). 이 선행 작업 없이 편향 감지를 주장하면 허위 |
| **인용 검증기의 "환각 차단" 가치** | 문장별 인용 검증기 | 실측 환각은 서술어(의미)에 있고 제안된 검사는 개체(URL·숫자·연도)에 있다. 실측 출력 'confirming the image's authenticity [E3]' 같은 근거 없는 단정이 화이트리스트 대조를 100% 통과 → **오히려 환각 문장에 "근거 확인됨" 도장을 찍는다** |
| **"문장 끝 라벨" 파서 계약** | 문장별 인용 검증기 | 실측 5회 전부 라벨이 **불릿 앞**에 붙었고 문장 끝은 0건. 출력이 마침표 문장이 아니라 개행 덩어리라 한국어 문장 분해가 성립하지 않는다 |
| **"검증된 폐기 경로 재사용"** | 문장별 인용 검증기 | `_source_evidence_ids`는 항상 비지 않으므로 `verified`는 프로덕션에서 항상 True. `policies.py:110-114`의 LLM_SUMMARY 분기와 폐기 분기는 **한 번도 실행된 적이 없다** |
| **"llm_assistant가 None이면 폴백"** | 6·9·16·23 공통 | `env_clients._llm_assistant()`는 도달성 검사 없이 무조건 반환. `is None` 분기는 절대 실행되지 않고 llm provider는 항상 `enabled=True` |
| **`EvidenceEnricher` / `ReviewEnrichmentJob` 수정** | 6·9·16 공통 | 프로덕션 미배선 죽은 코드. 실제 호출 지점은 `store_operations.py:471`. 테스트 2건 + `test_public_module_layout.py` 임포트 검사가 붙어 있어 이 범위에서 손대면 안 됨 |
| **"7B로 메모 초안"** | 작업별 모델 라우터 | CPU 프리필 100초+ 생성 40~50초. 1단계 실측 tok/s가 나오기 전에는 번들에 넣지 말 것(+4.7GB) |
| **"새 저장 인프라 없음"** | 의미지문 인덱스 | 신규 테이블은 `STORE_TABLES`·`CONSTRAINED_TABLE_SCHEMAS`·`LEGACY_REBUILD_ORDER`·`TYPED_COLUMNS`·`SCHEMA_VERSION`·`_validate_table`을 전부 건드리고, 벡터 BLOB이 백업 zip 10개에 곱해져 들어간다 |
**미검증 1건**: 16위 「밴드 임계값 스윕·섀도 리스코어」는 적대적 검증 단계에서 판정이 기록되지 않았다(`verdict: null`). 다른 항목과 같은 수준의 반증을 거치지 않았으므로 착수 전 별도 검증이 필요하다. 특히 원안이 전제하는 "결정된 케이스 전체"가 현재 0건이라는 점, `_rescore_all_submissions`가 항상 쓰기까지 한다는 점(read-only 경로 분리 필요)이 검증 대상이다.
## 단계별 로드맵 제안
> 이 로드맵은 심사 점수와 독립적으로 **선행조건 의존 관계**로 짰지만, 결과적으로 정합한다:
> 1단계는 심사 1·3·6·7·10·12위를 포함하고, 라벨 의존 항목(21·23·24위)은 전부 3단계로 밀린다.
> 점수 하위권이 3단계로 가는 이유는 "가치가 없어서"가 아니라 **지금은 입력이 없어서**다.
### 1단계 — 지금 당장(라벨 0건·의존성 0·모델 반입 0에서 작동)
한 스프린트짜리 "기반 정리" 묶음. **이 단계는 AI를 늘리지 않는다. 이미 켜져 있는 AI의 거짓말과 세금을 끄는 작업이다.**
| 항목 | 내용 | 공수 |
|---|---|---|
| 1-a | **LLM 입력 경계 단독 커밋** — 프롬프트 화이트리스트 + 증거 상한 12건 + `num_ctx` 명시 (`promotion_reason` 제외) | S |
| 1-b | **시드 LLM 순회를 `_write_lock` 밖으로** + 시드당 처리 상한 20건 + 실패 백오프 | S |
| 1-c | **`initialRiskScore`/`initialRiskBand`/`scorerVersion` 각인** + 라운드트립 속성 기반 테스트 | S |
| 1-d | **모델·프롬프트 버전 스탬프** (id 해시 오염 회피 규칙 준수) | S |
| 1-e | **원본 메타 권리추출 EXIF+IPTC만** (XMP는 별건, 스톡 ID는 +30 라벨) | S |
| 1-f | **`env_clients._llm_assistant()` 도달성 프로브** + provider `enabled` 산출식 정정 + `offline-install.md`의 잘못된 서술 정정 | S |
| 1-g | **요약 상태 3단 배지**(정상 / 생성 안 됨+원인 / 실패) — 5단은 과설계 | S |
| 1-h | **액션 플래너 축소판** — `plan()` 결과를 `review()` 응답에 싣고 `_is_generic_local_query` 탈락 쿼리 숨김 + `query_priority` 매핑 1줄 | S |
| 1-i | `app.js:726-746` 영→한 if-체인을 사전 테이블로 리팩터 *(같은 항목에 있던 "`_connect()`에 WAL 추가"는 **이미 적용돼 있어 삭제**)* | S |
**2단계로 가기 위한 선행조건**
- 1-d의 `latency_ms`가 실제 타깃 하드웨어에서 2주 이상 쌓여 **케이스당 실측 지연 분포**가 나올 것.
- 1-c의 라운드트립 속성 테스트가 그린이고, 새 `EvidenceSource` 배관 12곳 체크리스트가 문서로 확정될 것.
- 타깃 하드웨어 사양(코어/스레드/메모리대역/GPU 유무)이 확정될 것 — 실측이 2스레드 20.6초 vs 20스레드 5.9초로 갈렸다. 이 값 없이는 3B 채택 여부를 결정할 수 없다.
- 모델 번들 크기 상한이 승인될 것.
### 2단계 — 모델 도입을 수반하는 중기
| 항목 | 내용 | 비고 |
|---|---|---|
| 2-a | **YuNet 교체** + detect 중복 호출 제거 + tier 라운드트립 | 모델 345KB, 지연 순감. 밴드 재캘리브레이션 비용을 판정 라벨이 쌓이기 전에 치른다 |
| 2-b | **모델 번들 무결성 검증** — `models.lock.json`(`/api/tags` 응답 기반) + **비차단** 프리플라이트 + `enabled`와 분리된 `modelReady` 필드 | 차단형 프리플라이트 금지(DROP 방화벽에서 기동 정지) |
| 2-c | **잡 큐 + 백그라운드 워커** (`run_once()` 테스트 모드, 부팅 시 running→pending 리셋) | 1-b로 급한 불을 끈 뒤, 태스크가 2개 이상 생긴 다음 |
| 2-d | **모델 라우터 1단계** — `num_ctx`/`num_predict`/`keep_alive`/`timeout`을 환경변수 프로필로 노출 + 3B 실험 + 감사 로깅(모델명·소요시간·토큰수) | 7B는 실측 tok/s 확인 전 번들 금지 |
| 2-e | **증거팩 직렬화 + 인용 칩** (3B) — 줄 단위 출력(`[E<n>]`로 **시작**), 라벨 없는 줄만 드롭, 미라벨 줄은 렌더하지 않음(취소선 금지) | 1-a·2-d 선행 필수 |
| 2-f | **방송캡처 포렌식** — 실측 2.0ms/장, 모델 0. 단 **1차는 0점 라벨로만** 출시하고 워터마크와 같은 EvidenceSource·같은 GUI 그룹으로 배관 비용을 한 번만 지불 | 독립 카테고리 2개 이상 성립 규칙 |
| 2-g | **워터마크 로고 템플릿 매칭** — ROI를 4모서리 + 하단 20%로 한정, 활성 로고 12개 상한, 스케일 3단계, 점수 +45(단독 high 금지), 크롭은 face-crop과 **분리된 보관 경로** | OCR(56~72MB) 반입은 로고 경로 검출률을 본 뒤 결정 |
**3단계로 가기 위한 선행조건**
- **판정 라벨이 실제로 축적될 것** — 이것이 절대적 게이트다. 현재 0건. 프로파일 군집은 자체 규칙(군집 5건 이상)까지 감안하면 수백 건이 필요하고, 쿼리전략별 통계는 `queryStrategy`가 채워지기 시작한 이후 수개월이 걸린다.
- `decision_stats()`의 밴드×판정 교차표가 유의미한 표본(최소 100건)을 가질 것.
- 골든셋 diff 하네스가 돌아 "가중치를 바꾸면 큐가 어떻게 변하는가"를 숫자로 낼 수 있을 것.
- 임베딩 저장 금지 정책의 개정 여부가 **사용자 결정**으로 확정될 것(아래 열린 질문 참조).
### 3단계 — 학습·자동화로 가는 장기
| 항목 | 내용 | 진입 조건 |
|---|---|---|
| 3-a | **근거 신뢰도 카운트 배지** — (source, matchType, domainSource) 3키 집계, n≥15에서만 표시. isotonic 보정표·confidence 주입은 **만들지 않는다** | 사실상 오늘 출시 가능(n=0에서 조용히 사라짐). 오히려 라벨 수집 인센티브 |
| 3-b | **AI 수용률 축소판** — 새 테이블·새 화면 없이 `decision_stats`에 `llmDraftPulled`/`llmDraftMedianRetention` 두 필드. 단 `_latest_decision_memo`의 `' · '` partition 버그 선수정 | 판정 라벨 축적 |
| 3-c | **밴드 임계값 스윕 + 섀도 리스코어** — 쓰기를 타지 않는 read-only 경로로 분리, 적용은 항상 사람 클릭 + 감사 이벤트 | 결정 케이스 100건 + 별도 검증(현재 미검증) |
| 3-d | **근거프로파일 일관성 감시(A안만)** — 시그니처는 현행 코드가 실제로 채우는 필드로만. LLM 정책메모 초안은 **폐기**. actor/다중 사용자 인증은 **분리 발주** | 판정 수백 건 |
| 3-e | **트리아지 레인 — 규칙 우선** — 1차는 모델 없이 `decision_stats` 교차표 규칙("같은 밴드+같은 providerState에서 과거 승인률>80% & n≥10")으로, 동결 라벨 200건 초과 시 로지스틱 교체 | 피처 동결(판정 시점 스냅샷) + 워치리스트 역류 제외가 선행 |
| 3-f | **의미지문 인덱스** — 1단계는 제출물+KB 참조만(검색 후보 제외), cv2.dnn + CNN 백본, 벡터는 SQLite BLOB이 아니라 `.npy` 외부 저장, 점수 0 + '변형 의심' 라벨 | **거버넌스 사전 승인 없이는 착수 금지** |
| 3-g | **페이지 권리표기 추출** — 유사도 폴백 체인에 얹지 말고 "점수 기여 상위 N개 URL"에 대한 독립 페치 단계(케이스당 2건)로. 1차는 정규식 전용, LLM 없음 | 페치 커버리지 재설계 |
| 3-h | **케이스 Q&A — 고정 질문 사전 캐시** — 자유 질문 동기 대기(40~120초)는 기능의 사망 원인. 증거 확정 시점에 3~5개 고정 질문을 미리 생성해 클릭 시 0초 | 잡 큐(2-c) + 별도 캐시 테이블(TTL, 감사 대상 아님) |
## 결정 반영 실행안 (2026-07-25 확정)
아래 열린 질문 중 4개가 답변되었다. **이 답변들이 위 로드맵의 전제를 바꾸므로, 실행 시에는 이 절이 로드맵보다 우선한다.**
| 질문 | 결정 |
|---|---|
| LLM 역할 범위 | **증거 판독 + 구조화 판단** (요약만 유지 아님) — 3B 이상 승급 |
| 타깃 하드웨어 | **GPU 있음** |
| 새 신호의 점수 개입 | **0점 라벨로 시작**, `decision_stats` 상관 확인 후 가중치 부여 |
| 실사용 투입 | **곧 투입 예정** |
### 이 결정들이 무효화한 분석
1. **CPU 지연 분석 전체가 이 배포에는 적용되지 않는다.** 제약 2절의 실측(2스레드 20.6초 / 20스레드 5.9초, 3B 60~180초, "7B는 인터랙티브 불가")은 CPU-only 전제였다. GPU에서는 3B가 인터랙티브이고 7B도 후보다. **다만 지연이 사라져도 `_write_lock` 점유 문제는 남는다** — 락은 추론 시간에 비례할 뿐 원인이 아니다.
2. **"잡 큐/배치 선연산이 필수"라는 전제가 약해졌다.** 2-c(백그라운드 워커)의 우선순위를 낮추고, 1단계의 락 이동(5줄)만으로 당분간 버틴다.
3. **워터마크 OCR의 재검토 여지가 생겼다.** 단 반증된 결함(배포 CRNN 알파벳에 `©`·대문자 없음)은 하드웨어와 무관하므로, 재검토한다면 **다른 OCR 모델 선정이 전제**다.
4. **의미지문 인덱스는 여전히 막혀 있다.** 막는 것이 성능이 아니라 거버넌스(생체 템플릿 저장 금지 사양)이기 때문. GPU는 이 항목을 풀어주지 않는다.
### 🔴 실사용 투입 **전에** 끝내야 하는 것 — 되돌릴 수 없는 창
**"곧 투입"이 만든 가장 중요한 제약이다.** 아래 4건은 운영자가 첫 판정을 누르는 순간 **소급이 불가능하거나 비용이 급등**한다.
| # | 항목 | 왜 투입 전이어야 하는가 |
|---|---|---|
| **T0-1** | `initialRiskScore` / `initialRiskBand` / `scorerVersion` 각인 (`_submission_payload`) | `_rescore_submission`이 12곳에서 `riskScore`를 덮어쓰므로 저장된 점수는 이미 왕복 점수다. 원본 점수를 지금 각인하지 않으면 **판정이 시작된 뒤에는 영원히 복원할 수 없고**, 모든 점수 회귀 테스트가 항상 통과하는 빈 테스트로 남는다 |
| **T0-2** | YuNet 교체 + 밴드 재캘리브레이션 | 등급화는 기존 medium 다수를 low로 옮겨 밴드 분포를 바꾼다. 판정 이력이 쌓인 뒤에 하면 `decision_stats`의 캘리브레이션 기준선이 판정 이력과 어긋난다. **판정 0건인 지금이 이 비용이 가장 싼 시점이고, 그 시점은 곧 닫힌다** |
| **T0-3** | **판정 시점 피처 스냅샷 각인** (라벨 순환 차단) | 큐가 `riskScore` 내림차순 정렬이고(`app.js:1267`) 화면이 점수·밴드·reasons를 보여준 뒤 판정을 받는다. 이 구조에서 모은 라벨은 "이 근거가 반려를 예측한다"가 아니라 **"운영자가 내가 보여준 점수에 동의했다"**를 기록한다. 판정 순간 운영자가 실제로 본 것(점수·밴드·표시된 근거 목록)을 스냅샷으로 남겨야 나중에 순환 보정이 가능하다. **투입 후에 붙이면 그 이전 라벨은 전부 용도 불명이 된다** |
| **T0-4** | 모델·프롬프트 버전 + `latency_ms` 스탬프 | 투입 후 모델을 3B로 올리면 과거 요약과 신규 요약이 DB에 섞이는데 구분할 필드가 없다. 지금 스탬프를 넣으면 승급 전후 비교가 성립한다 |
> T0-3은 이번 심사에서 나온 항목이 아니라, **"곧 투입"이라는 답변이 새로 만든 요구사항**이다. 학습 계열 7건 전부의 미래 가치가 여기 걸려 있고, 비용은 S다.
### 실행 순서 (확정)
**A. 투입 전 (T0)** — `initialRiskScore` 각인 · YuNet + 재밴딩 · 판정 스냅샷 각인 · 버전/지연 스탬프
**B. LLM을 판독기로** — GPU 전제로 공격적으로 간다
- B-1 입력 경계(화이트리스트 + 상위 12건 + `num_ctx`) — `promotion_reason` 제외 필수
- B-2 3B 승급 + GPU 프로필(`num_ctx`/`num_predict`/`keep_alive`/`num_gpu`), 7B는 B-2 실측 후 판단
- B-3 시드 LLM 호출을 `_write_lock` 밖으로
- B-4 **구조화 증거 판독** — `format=json`으로 `{claims:[{text, evidence_ids[], kind}], needs_check:[...]}` 스키마 강제.
**인용 검증은 문장 파싱이 아니라 스키마 필드 검증으로 한다** — `evidence_ids`가 실제로 프롬프트에 들어간 증거 id 집합의 부분집합인지만 대조하고, 아니면 그 claim을 렌더하지 않는다.
*이 설계는 심사에서 3.03점을 받은 「문장별 근거 인용 검증기」의 반증 사유를 우회한다*: 그 안은 자유 텍스트를 문장 분해해 개체(URL·숫자)를 대조하는 방식이라 `'confirming the image's authenticity [E3]'` 같은 **근거 없는 서술어에 "확인됨" 도장을 찍는** 문제가 있었다. 스키마 필드 대조는 모델이 claim 단위로 id를 스스로 붙이게 하므로 이 실패 모드가 구조적으로 발생하지 않는다.
**C. 모델 없는 신호 (전부 0점 라벨)** — EXIF/IPTC 권리추출 · 방송캡처 포렌식. 두 항목은 **같은 `EvidenceSource`·같은 GUI 그룹**으로 묶어 배관 12곳 비용을 한 번만 지불한다.
**D. 투입 후 라벨 축적 뒤** — `decision_stats` 교차표로 0점 라벨들의 실제 반려 상관 측정 → 가중치 부여 → 임계값 스윕. 학습 모델은 T0-3 스냅샷이 수백 건 쌓인 뒤.
### 실기동 실측 (2026-07-25, GPU 타깃) — 추정치를 대체함
이 문서의 지연 수치는 전부 CPU 개발 머신 실측의 외삽이었다. 실제 GPU 타깃에서
저장된 실제 증거(제출물 #3, 증거 54행 중 12행 선별, 프롬프트 1,507자)로 재측정한 결과:
| 모델 | 지연 | 계약 준수 | 비고 |
|---|---:|---|---|
| `qwen2.5:0.5b-instruct` | 55.7s | ❌ | **프롬프트의 형식 예시(`<한국어 문장>`)를 그대로 출력**하면서 실제 라벨 12개를 전부 인용 |
| `qwen2.5:7b` | **8.8s** | ✅ | 근거 claim 2건 + 확인필요 1건. **채택** |
| `qwen3:8b` | 35.7s | ✅ | claim 4건이지만 4배 느림(thinking 모델) |
**이 실측이 바꾼 것 3가지**:
1. **기본 모델을 3B가 아니라 `qwen2.5:7b`로 정했다.** 3B는 이 빌드 머신에 없고 7B는
이미 있어서 에어갭 반입 비용이 0이며, GPU에서 8.8초면 인터랙티브다. 문서 상단의
"7B는 현재 근거로 인터랙티브 불가"는 CPU 전제였고 이 배포에는 적용되지 않는다.
2. **인용 검증만으로는 부족하다는 것이 드러났다.** 0.5B의 출력은 실제 라벨을 인용했으므로
집합 포함 검사를 **완벽히 통과**했다 — 완전히 비어 있는데도 "근거 있음"으로 판정됐다.
형식 예시 문자열을 상수로 두고 그 반향(echo)을 거부하는 가드를 추가했다.
*교훈: 그라운딩 검증은 "인용이 진짜인가"와 "내용이 진짜인가"를 둘 다 봐야 한다.*
3. **가짜 transport 테스트만으로는 이 결함을 절대 못 잡았다.** 스크립트된 응답은 언제나
잘 만들어진 JSON이기 때문이다. 실모델 프로브가 필요했다.
### 남은 결정 1개
**임베딩 저장 금지 정책의 개정 여부** — 의미지문 인덱스(22위)만 이 결정에 걸려 있고, 나머지 A~D는 전부 이 결정 없이 진행된다. 급하지 않으므로 D 시점에 재론해도 된다.
## 열린 질문 (사용자 결정 필요)
아래는 근거로 답할 수 없고 **사람이 정해줘야 다음 단계가 진행되는** 항목이다.
1. **타깃 하드웨어 사양** — CPU 코어/스레드 수, 메모리 대역폭, GPU 유무. 실측이 2스레드에서 20.6초, 20스레드에서 5.9초로 3배 이상 갈렸다. 이 값 없이는 3B 채택 여부(2-d)도, "케이스당 몇 초까지 허용되는가"도 결정할 수 없다. **현재 문서의 모든 저사양 추정치는 개발 머신 실측의 외삽이며 추정이다.**
2. **임베딩 저장 금지 정책의 개정 여부** — 3-f(의미지문 인덱스)는 사양 문서 두 곳(`docs/operations/image-rights-risk-filter.md:16`, `docs/project-introduction-and-technical-implementation.md:209`)의 "생체 템플릿 저장 금지" 문구와 정면으로 만난다. 전용 테이블 우회는 코드는 통과해도 사양은 통과하지 못한다. 결정 옵션: (a) 개정하지 않고 3-f 영구 배제, (b) "장면 벡터는 SEMANTIC_INDEX로 별도 관리"를 명문화하되 얼굴 박스 검은 마스킹 + 인물이 프레임 N% 이상이면 인덱싱 스킵 + 원본 삭제 시 연쇄 삭제를 조건으로. **(b)를 택하면 사양 문서 2곳 동시 개정이 필요하다.**
3. **케이스당 허용 가능한 추가 지연** — 동기 요청 경로에서 몇 초까지 운영자가 기다릴 수 있는가? 현재 `timeout=30`은 0.5B조차 여유가 없는 값이다. 이 숫자가 정해져야 (a) 어느 기능이 동기이고 어느 것이 배치인지, (b) 3B를 쓸 수 있는지가 결정된다.
4. **모델 번들 크기 상한** — 후보별 증분: 3B Q4_K_M +약 1.9GB / 7B +4.7GB / OCR ONNX 56~72MB / YuNet 345KB / 롤백 태그 유지 +379MiB. Ollama 런타임과 0.5B는 현행 구성이 이미 요구하므로 증분이 아니다. **반입 매체와 절차 기준으로 상한을 정해달라.**
5. **4번째 런타임 의존성 반입 승인 여부** — `requirements.txt` 헤더가 "서드파티는 PIL/cv2/numpy 뿐"이라고 선언하고 있다. 후보: `defusedxml`(25KB, XMP/C2PA 판독에 필수 — 없으면 폐쇄망에서 조용히 no-op), `onnxruntime`(40~55MB, CLIP/ViT 계열에 필수). 각각 별도 심사 안건이다.
6. **AI 제안의 판정 개입 범위** — 새 신호를 (a) 0점 라벨로만 낼 것인가, (b) 캘리브레이션 후 점수를 줄 것인가, (c) 준다면 단독 high 승격을 허용할 것인가. 검증자들은 워터마크 +70, 스톡 자산 ID +70, 방송캡처 +30, 의미유사도 +30을 **전부 낮추거나 0점 라벨로 시작하라**고 권고했다. 판정 라벨이 0건인 현재로선 어떤 가중치도 근거가 없다.
7. **새 `EvidenceSource` 배관 12곳을 언제 한 번에 지불할 것인가** — 워터마크·방송캡처·권리메타·권리표기가 각각 새 소스를 요구한다. 운영자 관점에서는 넷 다 같은 질문("이 이미지가 어디서 왔나")에 답하므로 **하나의 소스·하나의 GUI 그룹으로 묶어 배관 비용을 한 번만 지불**하는 안이 검증 단계에서 제시됐다. 묶을지 나눌지는 화면 설계 결정이다.
8. **다중 사용자 인증(actor)을 별도 발주할 것인가** — 현재 시스템에 사용자 개념이 없어(단일 공유 bearer 토큰) "운영자별 편향 감지"·"프리마킹과 사람 라벨 구분"이 원리적으로 불가능하다. 계정·로그인·세션→actor 전파(`add_audit_event` 호출부 약 10곳) + 감사 로그 마이그레이션은 별도 프로젝트이며 보안 리뷰 대상이다.
9. **골든셋 파일의 반출 통제 수준** — 1000건 기준 약 31MB, 내부 riskScore·result URL·pageTitle을 담는다. `.gitignore`만으로 충분한가, 아니면 납품 번들 생성 스크립트 제외 목록 + `offline-install.md` + delivery-audit 체크리스트까지 3중으로 갈 것인가.
10. **판정 라벨을 언제까지 얼마나 모을 것인가** — 3단계 전체(3-a~3-h)가 이 값에 걸려 있다. "몇 건이 모이면 학습계를 켠다"는 목표치를 정하지 않으면, 라벨 의존 기능 7건이 영구히 비어 있는 화면으로 남는다.