감사에서 나온 결론은 "그림은 지금 정확하지만 정확함을 지킬 장치가 없다" 였다.
코드 유래 리터럴 18개가 전부 코드와 일치하고 deliver 는 바이트 결정적인데,
드리프트를 막는 게이트·테스트·훅이 0개였고 문서에 적힌 재생성 절차는 그대로는
실행되지 않았다 — `archify` 실행 파일은 PATH 에 없고 실제로는 저장소 밖 전역
스킬을 node 로 불러야 한다. 다음 사람은 첫 명령에서 막혔을 것이다.
scripts/rebuild-diagrams.ps1 validate -> deliver -> visual-check -> manifest
scripts/update_diagram_manifest.py 수신증의 단일 작성자
docs/diagrams/manifest.json spec/html sha256 + generator 버전
tests/test_diagram_sync.py 게이트 14건
게이트는 브라우저도 Node 도 archify 도 없이 돈다. 산출물이 바이트 결정적이라
수신증 대조만으로 충분하고, 그래서 폐쇄망 CI 에서도 돈다. 두 사고 시나리오를
실제로 만들어 차단을 확인했다 — HTML 의 단어 하나를 고치면 html sha 가,
spec 의 태그 하나를 고치면 spec sha 가 어긋나 verify 가 실패한다. 노드 id 가
HTML 에 data-node-id 로 박히므로 의미적 연결도 함께 본다.
git 위생 두 건도 같이 정리했다. *.visual-check.json 이 개발 머신 절대 경로와
크롬 설치 경로를 평문으로 담은 채 미추적·비ignore 상태였다(24개) — 납품
저장소가 removable media 로 건너가는 구조라 한 번 커밋되면 제거가 어렵다.
그리고 15,000줄 생성 HTML 을 텍스트로 머지하려 드는 것을 막았다.
재생성해도 4종 sha256 이 전부 불변이라 바이트 결정성을 다시 확인했다.
감사에서 나온 나머지 부채(한글 폰트 미번들, 영어 뷰어 UI, architecture 좌표
하드코딩, lifecycle 손튜닝 8개소, 콘솔 CSP 비호환)는 PROJECT_MAP 부채 표와
다이어그램 절에 등재했다. 폰트와 UI 는 archify 쪽 한계라 spec 으로는 못 고친다.
검증: ruff clean, pytest 600 passed (게이트 +14).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BStHFrjQj2VypecSG9ex1V
두 갈래의 작업이 한 커밋에 들어 있다. llm_assistance.py 에서 T0-4 가 앞선
작업의 프롬프트 수정을 대체했기 때문에(프롬프트 리터럴이 llm_prompts.py 로
이동), 분리하려면 실제로 존재한 적 없는 중간 상태를 만들어야 한다.
## (1) 앞서 진행 중이던 review-intelligence 작업
- decision_stats(): 위험밴드 x 실제판정 교차표 + 캘리브레이션 지표
- cleared_reference 지식엔트리 타입: 이전에 클리어된 이미지 매칭은 +80 이
아니라 0점 긍정 신호
- LLM 요약 본문 영속화(_evidence_payload 에 summary/sourceUrls) — 그전까지
요약이 저장되지 않아 운영자에게 한 번도 보인 적이 없었다
- 운영자 GUI 반영
## (2) 실사용 투입 전 T0 — 첫 판정 이후에는 소급 불가한 것들
- T0-1 initialRiskScore/initialRiskBand/scorerVersion 각인.
_rescore_submission 이 12곳에서 riskScore 를 덮어쓰므로 저장된 점수는 늘
왕복 점수였고, 그것을 재채점과 비교하는 테스트는 항상 통과하는 빈 테스트다.
레거시 행은 scorerVersion="legacy" 로 표시만 하고 점수를 날조하지 않는다.
- T0-2 얼굴검출 Haar -> YuNet, 인물 등급(identifiable/background) 도입.
기존의 평평한 +35 는 사람이 찍힌 모든 사진을 medium 문턱 위로 올려 밴드
분포를 붕괴시켰다(실측 high 4 / medium 2 / low 0). 등급은 payload 라운드
트립으로 보존한다 — 잃으면 재채점마다 조용히 +35 로 되돌아간다.
가중치는 models/(gitignore) 에 두고 offline-install.md 에 sha256 반입 절차.
가중치가 없으면 Haar 로 강등하고 1회만 경고한다.
- T0-3 판정 시점 피처 스냅샷. 큐가 riskScore 내림차순이고 화면이 점수를 먼저
보여준 뒤 판정을 받으므로, 스냅샷 없이 모은 라벨은 "이 근거가 반려를
예측한다"가 아니라 "운영자가 내가 보여준 점수에 동의했다"를 기록한다.
- T0-4 모델/프롬프트 버전 + latency_ms 스탬프. latency_ms 는 _evidence_id
해시에서 제외한다 — 포함하면 같은 요약이 재생성될 때마다 id 가 바뀐다.
## (3) 임베딩 저장 금지 정책 개정
금지의 취지는 생체 식별이지 이미지 유사도가 아니므로 범위를 좁힌다.
장면 임베딩(전체 이미지 의미 벡터)을 이미지 유사도 판정에 한해 허용하되
얼굴 마스킹 / 인물 지배 프레임 제외 / 목적 제한 / 별도 저장 클래스 /
연쇄 삭제 5조건을 assert_scene_embedding_allowed 로 강제한다. 마스킹 증명이
없으면 거부한다 — 누락은 마스킹을 건너뛴 것과 구별할 수 없다.
얼굴 임베딩 금지는 landmarks/face_descriptor/face_encoding 추가로 강화.
pytest 502 passed (착수 전 463).
HIGH:
- face detection: guard OpenCV builds lacking CascadeClassifier and isolate
per-submission analysis failures in BatchAnalyzer, so a broken cv2 or one bad
image no longer takes down every seed/reload/import/upload/rerun with no HTTP
response
- risk scoring: gate Google custom image search (match=search_image) to 0
points so the persistence round-trip (result_url aliased to url) can no longer
flip the same evidence to +45 and inflate bands
- serialization: exclude SEARCH_SKIPPED/ENRICHMENT_FAILURE rows from the auto
search dedup signatures so a quota-skipped query stays retryable instead of
being treated as already done forever
- external_policy: add a daily-window reset + lock (mirrors SearchApiPolicy) so
Vision is not permanently disabled once the daily limit is hit
- image_store: skip a malformed manifest row (and coerce bad width/height)
instead of letting one ValueError paralyse loading/re-analysis for the queue
MEDIUM/LOW:
- enrichment: only count a face crop as transmitted when it actually left the
system, removing false usage debits and a false "personal image sent to
external provider" audit record; pick lastFailure from the failing item;
preserve the collection set when every search was quota-skipped; dedup
candidate counts; attribute Vision failures to the google provider badge
- store: search coverage requires a real result, not a failure row
- scoring: FACE_PERSON contributes once regardless of how many rows exist
- fingerprints: refuse near-matches on degenerate (near-uniform) average
hashes while keeping identical-hash matches
- preprocessing: apply EXIF orientation for external derivatives, face crops,
pHash and the PIL decode fallback
- promoter: scan title/description only, never the URL path/host tokens
- integrations: map Vision responses inside the adapter try; cap HTTPError
bodies; skip the connect probe when a proxy applies; reservation-style quota
accounting (try_reserve/release) to make the check-then-act atomic
- governance/admin: enforce the LLM-summary rule on the real persistence path;
reject unknown manual-entry fields; upsert the rejected_image knowledge entry
on repeat rejections; split policy skips out of the failures group
- env_file: strip a trailing inline comment from unquoted values
Environment drift fixed out-of-tree: removed the user-site OpenCV 5.0.0 that
shadowed the pinned opencv-python-headless==4.13.0.92.
Regression tests added; pytest tests/rights_filter is green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E6GD9gxUtfUiVB3JHejGpU
- Launcher guards Python 3.13 before importing the package and chdirs to the
repo root so cwd-relative default paths resolve consistently.
- main() logs the resolved .env path and warns on a missing static dir or a
short auth token; env_file reads utf-8-sig so a BOM doesn't drop the first
variable.
- .gitignore covers .claude/settings.local.json and .impeccable/.
Introduce a reusable browser-test harness under tests/operator_gui/:
- conftest.py: shared fixtures (operator_server, browser with graceful skip,
operator_page with screenshot-on-failure artifacts)
- pages/: OperatorWorkbench Page Object encapsulating selectors and actions
- refactor test_browser_smoke.py onto the fixtures + POM (drops duplicated
server/browser plumbing)
- test_decision_flow.py: approve/hold/reject E2E against the real server,
covering the reject-requires-memo guard and decision persistence
Pin playwright in requirements-dev.txt (Chromium bundled offline for the
air-gapped target) and ignore the artifacts/ screenshot dir.
Add requirements.txt (numpy/opencv-python-headless/pillow — the only
third-party runtime imports) and requirements-dev.txt, plus an offline
install runbook. Ignore .coverage and wheelhouse/.
Image rights / copyright detection system: SQLite store, HTTP app,
search integrations (Naver, Google Custom Search, Google Cloud Vision
web detection), image analysis (fingerprints, face/person detection,
evidence enrichment, risk scoring), an admin/review layer, governance
and retention policies, batch jobs, and a browser-based operator GUI.
This baseline incorporates a full code-review remediation pass
(46 fixes; 358 tests passing). Highlights:
CRITICAL
- Prevent evidence cascade-delete during the schema-constraint
migration by disabling FK enforcement around the table rebuild.
Security
- Sandbox served media (neutralize stored XSS from uploaded/collected
SVGs) via CSP + nosniff on the untrusted media routes.
- Strip embedded EXIF/GPS from external image derivatives before they
are sent to third-party APIs.
- Return a clean 404 (not an uncaught StopIteration) for PATCH on an
unknown provider.
Correctness
- LLM-summary failures no longer add +30 to the risk score.
- Decode only explicit JS escapes so Korean image URLs are not mangled.
- Consume search quota only after a successful request.
- Naver/Google adapters map responses inside the failure boundary, so a
malformed response degrades to evidence instead of crashing enrichment.
- Domain-aware provider attribution; face-box IoU de-duplication; count
searches (not result items); per-box crop isolation; clamp evidence
confidence and Google CSE num; real submittedEpoch; and more.
Robustness
- Offline LLM connect fast-fails (short connect timeout) so seed/reload
requests are not stalled; full read timeout preserved for generation.
- Malformed numeric env vars fall back to defaults instead of crashing
startup.
Performance
- Per-submission evidence reads (no full-table scan per rescore),
audit-log LIMIT, lazy active-store lookup, hoisted timestamps.
Tests
- ~24 regression tests added pinning the above fixes.
Runtime data (data/, outputs/, *.sqlite3, *.log), secrets (.env), and
node_modules are gitignored.