③ 파일명 stem 충돌: 다른 폴더의 같은 이름 파일이 한 건으로 합쳐져 둘째가 조용히 미심사였다.
id 는 재유도하지 않고(기존 판정 보존) id_collisions() 로 탐지해 bootstrap.intakeConflicts 와
파일당 1회 감사로 남기고, 콘솔 투입 상태줄에 경고 목록을 띄운다.
④ 행 버튼 '미사용' 이 false_positive 로 가 대표 근거 하나를 고르면 나머지 점수가 지워졌다 →
'사용'/'무관', 사용 재클릭은 pending(표시 해제, 점수 유지).
⑤ 분모 0 비율은 None → '측정 불가'. 외부 검색이 꺼진 케이스는 점수 0 유지, 한 줄 요약에
'외부 검색 꺼짐'.
부채: operator-review-workflow 13px 넘침 해소(겹치지 않는 레인 병합) → 다이어그램 5종 첫
REBUILD OK, v6 이전 카드 문구 교정. tests/conftest.py 로 로컬 Ollama 격리(가동 중에도 통과).
루트 VERSION 0.1.0 단일 출처, /health 와 기동 로그에 버전·스코어러 버전.
verify.ps1 (Ollama 가동 중, override 없음): ruff clean, 680 passed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BStHFrjQj2VypecSG9ex1V
코퍼스 DB 로 실 콘솔을 띄워 보니 큐의 '상위 근거' 칸이 '구글 visually s... 구글 페이지
with ...'(사유 원문 둘을 잘라 붙인 것)이었고 증거 제목도 '구글 full image match found' 로
절반 영어였다.
스코어러가 점수 계산과 별개로 집계(_Tally)해 riskBreakdown.headline 에
'웹 완전일치 사본 4곳 · 식별 가능 인물 · 이름 있는 검색 승격 2건 · 라벨 9개' 를 싣는다.
순서는 예산표, 웹은 가장 강한 뷰만 호스트 수(곳)로, 등록 일치·클리어 이력·미수행·실패·
신뢰 출처 제외까지 한 줄. 큐 행과 케이스 요약 보드의 첫 줄이 이것으로 바뀌고
headline 없는 옛 케이스만 예전 배치. formatReason 에 Vision 매치 4종·검색결과 사본·
기준 DB 유사도 한국어 매핑. 요약 보드 첫 줄은 word-break: keep-all 로 구 단위 줄바꿈.
점수·밴드 변화 없음(SCORER_VERSION 유지). 실렌더로 큐·케이스 확인.
verify.ps1: ruff clean, 663 passed.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BStHFrjQj2VypecSG9ex1V
이건 내가 만든 부채를 갚는 커밋이다. 스코어러에 풀 상한(v2/v3)과 신뢰 출처
제외(v4)를 넣으면서 증거 개수와 점수의 간극이 그 어느 때보다 벌어졌는데,
그 이유가 화면 어디에도 없었다. 운영자가 숫자를 믿지 못하면 그 우선순위는
우선순위가 아니고, 그러면 앞의 두 기능이 오히려 불신을 만든다.
ScoreResult.breakdown 을 신설했다. reasons 가 "어떤 증거가 말했는가"라면
breakdown 은 "그래서 몇 점인가"다.
지식DB 지문 매칭 80
웹 게시 (google) 60 원래 125 · 상한 60
웹 엔티티 라벨 20 원래 114 · 상한 20
신뢰 출처 posar.info 3행 제외
합계 115점이 상한 100에 걸렸습니다 — 이 구간의 케이스는 서로 구별되지 않습니다
마지막 줄이 특히 중요하다. 100 에 걸린 케이스들은 서로 구별되지 않으므로
우선순위 판단에 쓸 수 없는데, 지금까지는 그 사실이 숨어 있었다.
자리는 증거 요약과 증거 목록 사이다 - "51개 증거"를 읽은 직후, 목록을 훑기
직전이 바로 그 질문이 생기는 지점이다.
점수는 하나도 바뀌지 않는다. 계산 흐름에 기록만 붙였고 기존 스코어러 테스트
31건이 그대로 통과한다. 가중치·버킷·밴드가 불변이므로 SCORER_VERSION 은 v4 를
유지한다(설명을 추가하는 것은 스탬프 규칙의 대상이 아니다).
분해 합계가 항상 점수와 일치하는지를 테스트로 고정했다. 분해가 점수와 다른
이야기를 하면 그건 분해가 아니라 또 하나의 주장이 된다.
실렌더(Playwright 1440x900): 패널이 의도한 자리에 서고 콘솔 에러 0.
검증: ruff clean, pytest 642 passed (스코어러 5건 + 정적 1건 추가).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BStHFrjQj2VypecSG9ex1V
8/8 실효성 평가의 결론은 "검색·기록 노동은 줄여주나 심사 건수는 전혀 못 줄인다"
였다. 이 커밋은 그 축을 직접 건드리는 첫 기능이다.
중복 탐지는 이 도구에서 실증으로 검증된 유일한 판별 기능이다 — 재저장(JPEG q90)
/80%축소/50%축소/밝기+10% 변형 24건 중 24건 그룹핑, 같은 24건에서 SHA-256
완전일치는 0건. 그런데 그 결과를 쓰는 곳이 없었다. 서버는 store_duplicates.py
에서 priorDecisions 와 conflicting 을 계산해 내려보내는데, 콘솔은 그것을 큐
배지의 title 속성에만 넣었다. 마우스를 올린 사람만 "이거 지난주에 반려한
이미지"임을 알 수 있었고, 재제출은 처음부터 다시 심사됐다.
서버는 건드리지 않았다. 자리만 옮겼다.
큐 배지 "동일 이미지 3건 · 선례 반려" (title 이 아니라 본문에)
판정 패널 결정 메모 바로 위에 선례 배너 + [선례 승계] 1타
승계는 별도 판정 경로를 만들지 않는다. 메모를 채워 기존 setDecision 을 그대로
타므로 반려 메모 필수 검사 · 판정 시점 스냅샷 · 감사 이벤트가 유지되고, 승계
사실은 메모로 감사에 남는다("선례 승계: 동일 이미지 SUB-OLD1, SUB-OLD2 와 같은
판정").
판정 불일치는 승계 대상이 아니다. 같은 이미지에 서로 다른 판정이 있다는 것은
자동 처리할 상황이 아니라 사람이 봐야 하는 신호이므로, conflicting 이면 버튼을
아예 내지 않고 색을 갈라 표시한다.
실렌더 확인(Playwright 1440x900, 실제 서버 + 부트스트랩 스텁):
세 상태(승계 가능 / 판정 불일치 / 선례 없음)의 배너 문구와 버튼 유무가 의도대로,
승계 클릭 시 POST 본문이 위 메모 그대로, 콘솔 에러 0, 가로 오버플로 없음.
검증: ruff clean, pytest 622 passed (정적 3건 추가).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BStHFrjQj2VypecSG9ex1V
1) 긴 호스트가 제목을 세로 한 줄짜리 컬럼으로 만들었다
.evidence-title 은 flex-wrap 없는 flex 행이고 source-chip·host-chip 둘 다
축소를 거부한다(flex: 0 0 auto). 그래서 제목에 남는 폭이 수십 px 이 되고
overflow-wrap: anywhere 가 그걸 한 단어씩 세로로 쪼갰다. 컨테이너에
flex-wrap, 제목에 min-width: 14ch 를 줘서 제목이 자기 줄로 내려간다.
실렌더 측정 (같은 DOM 에 수정 전 CSS 런타임 주입, Playwright):
360px 제목 51x95px (5줄) -> 246x19px (1줄), 행 높이 95 -> 50
1280px 제목 9x380px (~20줄) -> 205x19px, 행 높이 380 -> 50
주석은 데스크톱에서는 해결됐다고 적혀 있었지만, 45자 CDN 호스트로는
1280px 에서도 재현된다 — 폰 전용 문제가 아니었다.
2) 20ch 로 잘린 호스트를 읽을 방법이 없었다
칩 title 이 "게시 호스트" 고정 문구여서, 20자 접두어가 같은 두 CDN 호스트를
링크를 열지 않고 구분할 수 없었다 — 모노스페이스를 고른 이유가 그 판별이다.
툴팁에 실제 호스트를 싣는다.
3) 클라이언트 호스트 폴백이 서버보다 짧았다
_result_host 는 (url, result_url, page_url, source_page_url, blogger_link,
image_url) 를 순회하는데 evidenceHost 는 3개만 봤다. host 키가 생기기 전에
저장된 네이버 블로그 행이 bloggerLink 만 가진 경우 칩이 조용히 사라졌다 —
"레거시 증거가 자기 출처에 눈멀지 않게" 라는 폴백의 목적과 반대다.
4) 보존된 케이스가 설명 없는 깨진 이미지였다
assetMissing 을 읽는 소비자가 GUI·API·내보내기 어디에도 없었다. 판정은 DB 에
남지만 콘솔에서는 "판정 후 파일 아카이브" 와 "업로드 손상" 이 구분되지
않았다. 큐 썸네일과 케이스 이미지에 표시 + 파생물 노트에 사유를 붙인다.
5) PROMPT_VERSION 갱신 (ko-v3-structured -> ko-v4-structured-host)
847f7a8 이 PROMPT_DATA_FIELDS 의 domain -> host 로 모든 증거 줄의 내용을
바꿨는데 스탬프는 그대로여서, 한 버전이 서로 다른 두 프롬프트를 가리켰다.
화이트리스트가 다른 모듈에 있어도 프롬프트의 일부라는 점을 llm_prompts
독스트링의 규칙에 명시했다. 렌더된 줄이 host= 를 담는지 보는 행위 테스트도
추가했다(튜플 멤버십 검사로는 안 잡힌다).
CLAUDE.md 요구대로 실모델 프로브(qwen2.5:7b, 로컬 ollama): grounded 3 /
dropped 0 / verified True, 형식 예시 반향 없음, 인용 라벨 정확(E1=full match).
다만 모델은 host 를 문장에 쓰지 않고 page_title 로 서술했다 — 호스트는
운영자 칩에는 닿지만 서술에는 아직 반영되지 않는다(SYSTEM_PROMPT 과제).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BStHFrjQj2VypecSG9ex1V
단일 파일이 프로젝트 상한(800줄)의 6.8배였다. 분할은 순수 라인범위 추출이며
어떤 구문도 수정·재배치·재포맷하지 않았다 — 15개 파트의 연결 결과가 원본
app.js 와 바이트 단위로 동일함을 확인하고 나눴다(125,689B).
클래식 스크립트(모듈 아님)라 하나의 전역 스코프를 공유하므로 로드 순서가 곧
계약이다. 원본에서 로드 시점에 실행되는 최상위 구문은 파일 끝의 리스너 등록
2개뿐이었고(DOMContentLoaded/hashchange), 나머지 참조는 전부 함수 본문 안이라
순서만 지키면 의미가 보존된다. 그 순서를 index.html 에 명시하고 테스트로 고정:
- test_index_loads_every_app_part_exactly_once_in_declared_order
각 파트가 정확히 1회, 선언 순서대로 로드되는지 + 디스크의 app-*.js 가
전부 배선됐는지 + 구 app.js 가 남아있지 않은지
- test_app_parts_stay_within_the_file_size_budget (800줄 상한)
정적 스펙 31곳은 app.js 본문을 문자열로 검사하고 있었다. 이 스펙들의 관심사는
'어느 파일에 있는가'가 아니라 '콘솔에 그 동작이 있는가'이므로, 파트를 로드
순서대로 이어붙여 읽는 _read_app() 으로 바꿨다.
검증: Playwright E2E 6건이 실브라우저에서 통과(콘솔 에러 0 단언 포함).
운영 콘솔 실렌더 확인 — 큐/케이스 심사 정상, 스크립트 15개 전부 200,
새로고침 후 콘솔 메시지 0건.