Commit graph

3 commits

Author SHA1 Message Date
changukyu
150e729aef fix: 신호 풀 도입으로 위험도 포화 해소 (SCORER_VERSION v1 -> v2)
기능 실효성 평가에서 확정된 판별력 붕괴를 고친다. 신호조합 224개 전수에서
77.7% 가 high, 56% 가 정확히 100점에 붙고 low 는 3.6% 뿐이었다. 큐 기본 정렬이
riskScore 내림차순인데 전건이 100 이면 정렬이 no-op 이므로, 점수가 운영자에게
주는 우선순위 정보가 0비트였다.

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

라벨 풀 (WEB_ENTITY_MAX_POINTS = 20)
  category 없는 raw Vision 웹 엔티티가 이름별 버킷이라 서로 dedup 되지 않고
  round(40*conf) 씩 무한 누적됐다. Vision 은 maxResults=10 이라 모든 이미지에
  라벨을 붙이므로, 이미지 매칭이 0건이어도 0.8 짜리 라벨 3개면 100/high 였고
  웹에 전혀 없는 오리지널 사진이 66/medium 이었다. 모집단 전체에 상수로
  존재하는 신호는 판별에 기여할 수 없다. medium 임계(30) 아래로 묶어 라벨이
  케이스를 물들일 수는 있어도 밴드를 정하지는 못하게 했다.

  category(character/celebrity/work) 가 붙은 엔티티는 풀에서 제외한다 — 그것은
  raw 라벨이 아니라 권리 분류이고 엔티티별 +45 를 유지한다. 조사 단계에서
  "test_two_distinct_web_entities_stack_into_high_risk 가 이 동작을 고정하므로
  함께 개정해야 한다"고 봤으나 틀렸다. 그 테스트는 category 있는 엔티티를
  다루므로 두 분기를 가르면 개정 없이 통과한다.

웹 존재 풀 (WEB_PRESENCE_MAX_POINTS = 60)
  Vision 응답 1회가 "이 이미지가 웹에 있다"는 하나의 사실을 full / partial /
  page / visual 4개 상관된 뷰로 동시에 보고하는데, 스코어러가 이를 독립 신호로
  합산해 45+35+35+10 = 125 -> 상한 100 이 됐다. 널리 게시된 평범한 이미지와
  명백한 침해물이 같은 100/high 로 수렴한 이유다. high 임계(70) 아래로 묶었다.

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

측정 (224조합, v1 -> v2)
  웹 신호만으로 high 가 되는 조합   2/7 -> 0/7
  100점 포화                        66.5% -> 51.3%
  웹에 없는 오리지널 사진, 라벨만   100/high -> 20/low
  이미 안전 확인한 자산(cleared)    100/high -> 20/low
  웹 존재 광범위(4개 매치 타입)     100/high -> 60/medium
  완전일치 사본 + 식별가능 인물      80/high ->  80/high (불변)
  명백한 침해(전 신호)              100/high -> 100/high (불변)

  전체 high 비율은 82.6% -> 76.3% 로 크게 안 움직인다. 이 격자는 신호 축을 균등
  샘플링하므로(지식DB 강매칭이 1/4) 실제 큐 분포의 대리값이 아니며, 실 DB 6건도
  100x4 -> 95x4 로 밴드가 바뀌지 않는다 — 그 4건은 전부 "완전일치 사본 + 인물"
  이라 high 가 맞다. 고친 것은 거짓 high 이지 진짜 high 가 아니다.

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

라운드트립 버킷 보존 테스트(test_review_fixes.py)는 신뢰도 1.0 에서 두 엔티티가
모두 캡에 붙어 성질이 가려졌다. 검증 대상은 점수가 아니라 엔티티 이름 버킷이
직렬화를 넘어 살아남는가이므로, 풀 아래(신뢰도 0.2)에서 재작성하고 동일 엔티티
반복이 여전히 1건으로 접히는지도 함께 본다.

테스트 5건 추가. pytest 570 passed(착수 전 565), 린트 클린.
2026-08-09 01:39:02 +09:00
changukyu
02395ac3aa fix: 판정 기록 소실 · 비상차단 미작동 · 생체 크롭 보존기간 초과 차단
"이 도구가 실제 업무에 도움이 되는가"를 6축으로 조사하고 축마다 결론을
반증시켰다(docs/reviews/2026-08-08-utility-assessment.md). 결론은 "검색·기록
노동은 줄여주나 심사 건수는 못 줄인다"이고, 그 과정에서 실행으로 확정된
P0 결함 4건을 여기서 고친다. 판별력 붕괴(P1)는 별도 작업이다.

폴더 정리가 판정 기록을 지웠다 (sqlite_store.py)
  _prune_missing_submission_files 가 원본 파일이 없는 submission 행을 판정
  여부와 무관하게 delete 했고, foreign_keys=on 이라 evidence 가 cascade 로
  함께 사라졌다. 심사 끝난 이미지를 아카이브로 옮기는 평범한 운영 행위가
  승인/반려를 DB 와 내보내기 양쪽에서 지운다. 실증: rejected 판정 후
  export 1행 -> 파일 제거 -> submissions 0행 / evidence 0행 / export 0행.
  판정된 케이스는 파일이 없어도 보존하고 assetMissing 마커를 남긴다. 마커가
  필요한 이유는 이 스윕이 매 리로드마다 돌기 때문이다 — 표시 없이 보존만
  하면 같은 감사 이벤트가 리로드마다 쌓여 500행 캡을 먹는다.

비상 차단 스위치가 아무것도 막지 않았다 (store_enrichment.py)
  emergency_disable_external_providers 는 DB 의 enabled/operatorDisabled 만
  쓰는데 자동 보강 경로가 그 플래그를 아예 읽지 않았다. 게이트가 걸린 곳은
  수동 검색뿐이라, 버튼을 눌러도 폴더 리로드·업로드·rerun-enrichment 가
  그대로 Naver/Google 로 나갔다. 스텁 어댑터 대조실험에서 CONTROL 5회 /
  차단 후 5회로 완전히 동일했다.
  _external_calls_blocked(provider_id) 를 신설해 자동 외부 호출 5개 경로에
  배선했다: auto naver, auto CSE, rerun Vision, 얼굴 크롭 웹탐지, 시드 배치
  (후자는 ExternalApiPolicy(disabled=True) 로 전환).
  enabled 가 아니라 operatorDisabled 를 본다. enabled 는 연결성/헬스에도
  반응하므로 운영자가 멈추라고 한 적 없는 트래픽까지 죽인다. 얼굴 크롭은
  생체 파생물을 전송하므로 상위 게이트에 의존하지 않고 독립 게이트를 건다.

생체 크롭이 보존기간을 넘겨 살아남았다 (store_backup.py)
  스냅샷이 face-crops 를 통째로 담는데 프루닝은 나이가 아니라 개수 10개
  기준이라, 90일에 파기됐어야 할 크롭이 암호화 없는 zip 안에 남았다. 복구는
  더 나빴다 — copyfileobj 로 새로 쓰므로 mtime 이 리셋되고, purge 가 mtime 을
  보므로 파기 대상이 복구 직후부터 다시 90일을 살았다.
  스냅샷에서 제외하고, 복구 시에도 레거시 아카이브의 크롭을 건너뛴다. 크롭은
  원본에서 재분석으로 재생성되므로 손실 비용이 없다.

오프라인 설치 문서가 계약 위반 모델을 지목했다 (offline-install.md)
  qwen2.5:0.5b-instruct 반입을 지시했는데, 같은 리포가 이 모델을 55.7초 +
  프롬프트 형식 예시 반향(출력 계약 위반)으로 기록해 두고 기본값을 7b 로
  고정해 뒀다. 문서대로 반입하면 타깃에서 요약이 조용히 죽는다. 모델명을
  정정하고, 반입물 목록에 없던 ollama 바이너리와 GPU 전제를 추가했으며,
  "env 를 비우면 LLM 없이 돈다"는 서술을 실측으로 정정했다 — 실제로는
  127.0.0.1:11434 로 폴백해 provider 가 enabled=True 로 보고된다.

회귀 테스트 3건 추가. pytest 565 passed(착수 전 562), 린트 클린.
2026-08-09 01:23:21 +09:00
changukyu
76a06f2ff1 docs: CLAUDE.md / PROJECT_MAP 신설, 7/11 체크리스트 재확인
세션 진입 문서가 없어 41.6k 라인 · 70모듈 구조를 매번 재발견하고 있었다.

- CLAUDE.md: 진입점 + 이 프로젝트에만 해당하는 규칙(폐쇄망 의존성 금지,
  app-*.js 로드 순서 계약, SCORER_VERSION 갱신 의무, 직렬화 라운드트립 불변식,
  LLM 은 실모델로 프로브, cp949 비ASCII 출력 주의)
- docs/PROJECT_MAP.md: 파일·라우트·스키마 위치, app-*.js 로드 순서, 알려진 부채
- README: verify 명령, app-*.js 구조, 인증 스로틀 동작 설명

7/11 리메디에이션 체크리스트를 코드와 대조해 갱신했다. 미체크 항목 중 6건은
이미 수정돼 있었고(naver 멀티페이지 쿼터, #10 Origin/Referer CSRF, env_file
BOM, daily-limit fail-open, __main__ 경로 앵커링, 그리고 Batch 8 전체가
Batch 6 과 중복) 각 줄에 확인 근거를 남겼다. 남은 미체크는 전부 근거가 기재된
명시적 연기다.
2026-08-08 09:49:55 +09:00