POSA_Copyrighter/docs/PROJECT_MAP.md
changukyu 624f435b82 docs: 부채 표의 두 수치를 직접 실측값으로 정정
감사 에이전트가 낸 수치를 그대로 부채 표에 옮겼는데, 저장소에 사실로 박히는
값이라 직접 재현해 확인했다.

  architecture 라벨 임계: 19·20자 통과, 21·22자 하드 실패 — 주장대로였다.
  lifecycle 캐스케이드: 상태 1개 + 전이 1개 추가에 오류 5건, 그중 손대지 않은
  기존 t-reject 관련이 3건. 표에는 4건으로 적혀 있었다.

검증 조건(waiting 레인 col 1)도 함께 적어 다음 사람이 같은 실측을 재현할 수
있게 했다. 캐스케이드가 실재한다는 결론은 그대로다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BStHFrjQj2VypecSG9ex1V
2026-09-21 11:33:32 +09:00

230 lines
16 KiB
Markdown
Raw 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.

# PROJECT MAP
> 어디에 무엇이 있는지에 대한 참조 문서. **작업 전 여기서 위치를 찾고 필요한 부분만 연다**
> (전체 소스 재스캔 금지). 새 파일·라우트·스키마가 생기면 해당 섹션을 갱신한다.
>
> 무엇을 했는지의 기록은 [INDEX.md](INDEX.md)에 있다. 이 문서는 **현재 구조**만 다룬다.
## 한 줄 요약
이미지 저작권 위험을 자동 분석하고, 운영자가 콘솔에서 근거를 확인해 승인/보류/반려를
판정하는 폐쇄망(air-gapped) 도구. Python 3.13 표준 `http.server` + SQLite + 정적 SPA.
런타임 서드파티는 이미지 처리 3종(`numpy`/`opencv-python-headless`/`pillow`)뿐.
## 검증 (단일 명령)
```powershell
.\verify.ps1 # ruff + pytest 전체 — 커밋 전 게이트, CI도 이 명령
.\verify.ps1 -SkipLint # 빠른 내부 루프
.\verify.ps1 -Coverage # src/ 커버리지 포함
```
설정은 `pytest.ini`(testpaths·pythonpath·timeout)와 `ruff.toml`(py313·line-length 120)에 있다.
## 실행
```powershell
python run_copyrighter_server.py # 기본 127.0.0.1:9500
```
런처가 `PYTHONPATH=src`와 작업 디렉터리를 고정한다. 옵션: `--host --port --db --images --static --env`.
---
## 소스 지도 (`src/rights_filter/`)
### `analysis/` — 이미지 분석과 위험도 산출
| 파일 | 역할 |
|---|---|
| `preprocessing.py` | 디코드, 외부 전송용 파생물 생성(메타데이터 제거), 얼굴 크롭 파생물 |
| `fingerprints.py` | 이미지 지문. `_perceptual_hash`는 **DCT pHash가 아니라 8x8 평균해시(aHash)** — 이름과 실제가 다름(알려진 부채) |
| `face_person_detection.py` | YuNet(ONNX) 얼굴 검출 + 인물 등급(`identifiable`/`background`). 가중치는 `models/`(gitignore) |
| `internal_analyzer.py` | 내부 분석 오케스트레이션 |
| `risk_scoring.py` | `RiskScorer.score()` — 점수·밴드 산출. **`SCORER_VERSION` 은 가중치/버킷/밴드 변경 시 반드시 올린다** |
| `evidence_normalizer.py`, `evidence_enrichment.py` | 증거 정규화·보강 |
| `search_query_generation.py`, `search_result_promoter.py` | 자동 검색 쿼리 생성, 검색 결과의 증거 승격 |
| `llm_assistance.py`, `llm_prompts.py` | LLM을 **요약기가 아닌 증거 판독기**로 사용. 입력 화이트리스트 9필드 + 상위 12건 상한, `format=json` 구조화 인용, 라벨 집합 포함 검사로 미인용 claim 폐기 |
| `provenance.py` | 출처 신호 — EXIF/IPTC 권리 표기 추출(화이트리스트 6필드), 방송캡처 포렌식. **둘 다 현재 0점 라벨**(캘리브레이션 전) |
| `derivatives.py`, `reason_builder.py` | 파생물 기록, 사유 문자열 조립 |
### `server/` — HTTP + 저장소
`http_app.py` (559줄) 가 라우팅·인증·보안 헤더를 담당하고, 저장소는 `CopyrighterStore`
하나를 **믹스인 8개**로 조립한다 (`sqlite_store.py:77`).
| 파일 | 역할 |
|---|---|
| `http_app.py` | `build_server()`, `CopyrighterHandler`. 라우팅 / Bearer+세션쿠키 인증 / CSP·CSRF / 바디 상한 10MB |
| `auth_rate_limit.py` | 실패한 인증 시도 스로틀(고정 윈도우 10회/60초, 클라이언트 주소별) |
| `sqlite_store.py` | `CopyrighterStore` 조립 + 시드·리스코어 |
| `store_schema.py` | DDL·제약·타입 컬럼 마이그레이션. `SCHEMA_VERSION` |
| `store_persistence.py` | `_put`/`_all` 등 저수준 upsert (PK 충돌 시 update) |
| `store_serialization.py` | 도메인 ↔ DB payload 왕복. **라운드트립은 원 점수를 재현해야 한다**(과거 위반 이력 있음) |
| `store_enrichment.py` | provider 상태 동기화, 얼굴 증거/크롭, 재분석, 자동 Naver/Google 검색 드라이버 |
| `store_operations.py` | 운영자 액션(판정·메모·재분석 요청) |
| `store_workflow.py` | 일괄 판정(`MAX_BATCH_DECISIONS`), 워크플로 |
| `store_search_candidates.py` | 검색 후보 수집·유사도 비교 |
| `store_page_scrape.py` | 후보 페이지/스타일시트 파싱 |
| `store_remote_fetch.py` | **네트워크 경계**. SSRF 가드 — 사전 URL 검사 + `_guarded_create_connection`(단일 DNS 해석으로 rebinding TOCTOU 차단) + 리다이렉트 홉 재검증 |
| `store_queue.py`, `store_duplicates.py`, `store_backup.py`, `store_text.py`, `store_url_utils.py`, `store_constants.py` | 큐, 지문 중복, 백업/복구, 텍스트·URL 유틸, 상수 |
| `image_store.py` | 제출 이미지 파일 저장소 + manifest |
| `env_file.py` | `.env` 로더 (utf-8-sig — BOM이 첫 키에 붙지 않도록) |
### `integrations/` — 외부 어댑터
`naver_search.py`, `google_custom_search.py`, `cloud_vision_web_detection.py`,
`web_detection_result_mapper.py`, `http_json.py`.
쿼터/정책은 `search_policy.py`(`SearchApiPolicy` — `try_reserve`/`release` 로 원자적 예약)와
`external_policy.py`(`ExternalApiPolicy`). **둘 다 날짜 경계 윈도우 리셋 있음**.
`env_clients.py` 가 `.env` → 어댑터/정책 조립(`build_provider_runtime`).
### `domain/` · `governance/` · `jobs/` · `admin/`
- `domain/records.py` — `Evidence`, `EvidenceSource`, `ScoreResult` 등 핵심 레코드
- `governance/` — 접근·보정·보존 정책 (얼굴 크롭 보존기간 등)
- `jobs/batch_analyzer.py` — 배치 분석, `retry_policy.py`, `usage_limits.py`
- `admin/` — 운영자 리뷰 핸들러·프레젠터
---
## 운영자 콘솔 (`web/operator-gui/`)
빌드 스텝 없음. 전부 클래식 스크립트(모듈 아님)라 **하나의 전역 스코프를 공유**한다.
`index.html` → `styles.css` + 보조 스크립트 4개(`operator-labels.js`, `submission-import.js`,
`evidence-guidance.js`, `operator-search.js`) → **`app-*.js` 15개**.
### `app-*.js` 로드 순서 (변경 금지 — 순서가 곧 계약)
```
app-state.js 상태·라벨 임포트 (나머지 전부가 읽는다)
app-api.js 토큰 프롬프트, fetch 래퍼, bootstrap/refresh
app-format.js 커버리지 계산, HTML/URL 이스케이프, 라벨 포맷터
app-selectors.js 상태 파생 조회 (선택 케이스, 위험/출처 클래스, 증거 필터)
app-render-queue.js 큐 목록, 임포트 상태, 케이스 리뷰 셸
app-render-evidence.js 증거 그룹화·정렬·마크업
app-render-panels.js 지식DB·후보·보정·provider·감사 패널, renderAll
app-navigation.js 뷰/탭 전환, 케이스 선택, 판정, 수동 검색, 재분석
app-keyboard-batch.js 키보드 심사 모드, 중복 그룹, 일괄 판정
app-decision-tools.js 메모 프리셋, 판정 내보내기, 백업, cleared reference, LLM 초안
app-stats.js 판정 통계·캘리브레이션
app-submissions.js 제출물 리로드/임포트/업로드, 지식 항목 생성
app-collections.js 후보 수집·기각·선택·승격
app-admin.js 지식/보정/provider/증거상태 관리 액션
app-bootstrap.js bindEvents, init, DOM 리스너 ← 유일하게 로드 시점에 실행됨
```
이 순서는 `tests/operator_gui/test_static_workbench.py` 의
`test_index_loads_every_app_part_exactly_once_in_declared_order` 가 고정한다.
파일을 추가·제거·재배치하면 그 테스트의 `APP_SCRIPT_NAMES` 와 `index.html` 을 함께 고친다.
---
## HTTP 라우트 (`http_app.py`)
인증: `COPYRIGHTER_AUTH_TOKEN` 설정 시 `/api/*` 와 미디어 라우트는 Bearer 헤더 또는 세션 쿠키
필요. 정적 셸과 `/health` 는 열려 있다(운영자가 토큰을 입력할 수 있어야 하므로).
**GET** — `/health` · `/api/bootstrap` · `/api/review-queue` · `/api/submissions/{id}/review` ·
`/api/providers` · `/api/providers/health` · `/api/audit-events` · `/api/decision-memo-presets` ·
`/api/decisions/export` · `/api/admin/backups` · `/api/stats/decisions` ·
`/media/` · `/knowledge-media/` · `/collected-media/` · `/face-crop-media/`
**POST** — `/api/submissions/{id}/search-auto|rerun-enrichment|decision|clear-reference` ·
`/api/submissions/reload|import-folder|upload-image|decision-batch` ·
`/api/evidence/{id}/status` · `/api/search/manual` · `/api/knowledge/manual` ·
`/api/knowledge/{id}/promote-watchlist|exclude-watchlist|deactivate|reactivate` ·
`/api/collections/keyword` · `/api/collections/candidates/promote-batch` ·
`/api/collections/candidates/{id}/promote|dismiss` ·
`/api/decision-memo-presets` (+ `/{id}/delete`) · `/api/admin/backup` ·
`/api/providers/emergency-disable`
**PATCH** — `/api/knowledge/{id}`
---
## SQLite 스키마 (`store_schema.py`, `SCHEMA_VERSION = 2`)
제약 있는 테이블 7개 — `submissions`, `evidence`, `providers`, `knowledge_entries`,
`collection_candidates`, `corrections`, `audit_events`. 추가로 `submission_queues`.
각 테이블은 조회·정렬용 타입 컬럼 + 전체 도메인 객체를 담은 `payload` JSON 컬럼 구조.
`evidence.submission_id` 는 `submissions(id)` 에 `on delete cascade`.
WAL 적용됨. DB 기본 경로는 `data/copyrighter.sqlite3`(gitignore).
---
## 테스트 (`tests/`, 562건)
- `tests/rights_filter/` — 단위·통합. `conftest.py` 가 `src` 를 sys.path 에 넣는다
- `tests/operator_gui/` — 정적 검증 + Playwright E2E.
- Page Object: `pages/workbench_page.py` (`OperatorWorkbench`). 업로드 대기는
`UPLOAD_ANALYSIS_TIMEOUT_MS`(30초) — 업로드는 서버에서 실분석을 돌리므로 일반 대기보다 길다
- Chromium 없으면 **자동 skip**(`importorskip`)
- 정적 테스트는 `app-*.js` 전체를 이어붙여(`_read_app()`) 내용을 검사한다 —
어느 파일에 있는지가 아니라 콘솔에 그 동작이 있는지를 본다
---
## 다이어그램
소스 JSON 과 산출물 HTML 이 같은 폴더에 쌍으로 있다. **HTML 직접 수정 금지** —
고칠 것은 언제나 spec JSON 이고, 재생성은 스크립트 하나다:
```powershell
.\scripts
ebuild-diagrams.ps1 # validate -> deliver -> visual-check -> manifest
.\scripts
ebuild-diagrams.ps1 -SkipVisualCheck # 브라우저 없는 호스트
```
게이트 두 개가 이 계약을 강제한다(둘 다 브라우저·Node·archify 없이 도는 순수 비교):
- `tests/test_diagram_sync.py` — **산출물이 spec 보다 낡았나.** `manifest.json` 의
sha256 대조. HTML 을 직접 고치거나 spec 만 고치고 재생성을 잊으면 막힌다.
- `tests/test_diagram_facts.py` — **spec 이 코드보다 낡았나.** 그림에 하드카피된
코드 유래 값(SCORER_VERSION, SCHEMA_VERSION, 믹스인 개수, `app-*.js` 개수,
바디 상한, 프롬프트 12행 상한, 풀 상한, 기본 모델, 런타임 의존성 3종)을
실제 코드 상수와 대조한다. 지식DB 엣지의 "0.9+ → 80점" 은 상수가 아니라
인라인 리터럴이라 실제로 채점해서 확인한다.
네 시나리오(HTML 직접 수정 · spec 만 수정 · SCORER_VERSION 상승 · 프롬프트 상한 변경)
모두 실제로 만들어 차단을 확인했다.
**문서 재생성 전용 요구사항** (런타임 아님, 폐쇄망 타깃에는 불필요):
Node 18+ 와 archify 스킬(`$env:ARCHIFY_HOME`, 기본값 `~/.claude/skills/archify`),
visual-check 단계에만 Chrome/Chromium. 산출 당시 generator 는 `archify 2.17.0-dev.1`
이고 `manifest.json` 에 기록된다 — 스킬 버전이 바뀌면 같은 spec 에서도 바이트가
달라지므로, 해시가 깨졌을 때 코드 변경과 툴 변경을 구분하는 근거가 된다.
**용도: 내부 문서다 (2026-09-21 결정).** 고객 인수물이 아니므로 납품 품질 기준을
적용하지 않는다 — 아래 부채 표의 한글 폰트·영어 뷰어 UI 항목이 low 인 이유가 이것이다.
인수물로 승격하면 그 둘이 먼저 blocker 가 되므로, 승격 시 이 줄부터 고친다.
모두 자체 완결 HTML 이라 외부 요청이 없다(9/21 감사에서 실측: src/href/@import/fetch
모두 0건). 단 **file:// 로 여는 산출물**이다 — 운영자 콘솔의 static_root 로 서빙하면
app-shell CSP(`script-src 'self'`, `http_app.py:514`)가 인라인 스크립트 4개를 차단해
상호작용이 죽는다. 서빙이 필요하면 CSP 완화가 아니라 별도 라우트에서 nonce 정책을 쓴다.
| 파일 | 내용 | 타입 |
|---|---|---|
| `evidence-dataflow.html` | 증거 데이터 흐름 5단(수집·경계 검사·정규화/채점·영속화·소비). 라운드트립 불변식이 중심 | dataflow |
| `system-architecture.html` | 시스템 구성. 폐쇄망 경계와 그 밖의 외부 검색 API, SSRF 가드 앞의 호출 게이트 | architecture |
| `operator-review-workflow.html` | 운영자 심사 4레인(제출·자동·심사·차단). 비상 차단 시 심사가 이어지는 경로 포함 | workflow v2 |
| `submission-lifecycle.html` | 제출물 상태 전이. 분석 실패·보류가 종료가 아니라 분석으로 복귀하는 두 고리 | lifecycle |
## 알려진 부채
| 항목 | 위치 | 비고 |
|---|---|---|
| `_perceptual_hash` 가 pHash가 아닌 aHash | `analysis/fingerprints.py:47` | 균일/저디테일 이미지 유사도 오탐. 교체 시 0.9 임계 재보정 필요 |
| src 4파일이 800줄 초과 | `store_page_scrape.py`(972) `store_operations.py`(968) `sqlite_store.py`(904) `store_enrichment.py`(853) | 저장소 코어라 분할 리스크 있음. 별도 작업으로 |
| 밴드 임계·풀 상한 미캘리브레이션 | `analysis/risk_scoring.py` | 판정 이력 0건이라 70/30 임계가 실제 판정과 대조된 적 없다. v3 의 두 풀(`WEB_PRESENCE_MAX_POINTS` 60 은 **제공자별** / `WEB_ENTITY_MAX_POINTS` 20)은 신호의 구조적 성질로 정한 값이고 상관 검증 전이다 — 판정 30~50건 후 `decision_stats` 교차표가 첫 근거가 된다 |
| 출처 신호 2종 0점 | `analysis/provenance.py` | 캘리브레이션 전. 실측 코퍼스에서 가치 미입증. 방송캡처는 `MIN_CAPTURE_CATEGORIES=2` 에서 geometry 가 1개 카테고리라 자막 없는 캡처는 구조적으로 도달 불가 |
| 테스트가 로컬 LLM 에 실접속 | `integrations/env_clients.py:260` | `_llm_assistant` 가 항상 `OLLAMA_BASE_URL` 기본값(`127.0.0.1:11434`)으로 클라이언트를 만들므로, provider_runtime 없이 만든 store 를 쓰는 테스트는 **ollama 가 떠 있으면 실호출**을 한다. `test_http_app.py` 3건이 그 상태에서 소켓 타임아웃으로 실패한다(`OLLAMA_BASE_URL=http://127.0.0.1:1` 로 돌리면 통과). 폐쇄망 프로젝트의 테스트가 외부 서비스의 **부재**에 의존하는 상태 |
| 다이어그램에 한글 폰트 미번들 (내부 문서라 low) | `docs/diagrams/*.html` | 임베드된 JetBrains Mono 의 unicode-range 6개에 **한글 구간이 없다**(`U+AC00` 매치 0). 폴백 스택은 중국어 폰트뿐(`Noto Sans Mono CJK SC`/`PingFang SC`/`Microsoft YaHei`). 렌더 텍스트의 40~72% 가 한글이라 타깃 폰트에 의존한다 — 한국어 폰트 없는 최소 설치 호스트에서는 두부(tofu). archify 에 저자용 폰트 옵션이 없어 spec 으로는 못 고친다(레이아웃 폭은 전각으로 측정되므로 깨짐이 아니라 글자 문제). **내부 문서로 확정(9/21)** 되어 현재는 low — 사내 머신에는 한글 폰트가 있다. 인수물로 승격하면 blocker |
| 뷰어 UI 가 영어 고정 (내부 문서라 low) | `docs/diagrams/*.html` | `locale` enum 이 `en`/`zh-CN` 뿐이고 템플릿이 `<html lang="en">` 하드코딩. 저자 내용은 한국어인데 조작 UI·aria 라벨·범례 제목은 영어. 스크린리더가 한글 본문을 영어로 읽는다 |
| architecture spec 만 좌표 하드코딩 | `docs/diagrams/system-architecture.architecture.json` | 컴포넌트 11개 전부 `pos`+`size` 수기, `size` 는 [124,62] 고정. 라벨 **21자부터 validate 하드 실패**(19·20자 통과, 21·22자 실패 — 9/21 실측. 현재 `store_remote_fetch` 18자 → 여유 2글자). workflow spec 은 기하 하드코딩 0개이므로 같은 저장소에 더 나은 패턴이 있다 |
| lifecycle 기하 손튜닝 8개소 | `docs/diagrams/submission-lifecycle.lifecycle.json` | `labelDy`×5·`channelX`×2·`yOffset`×1 이 현재 배치 전제의 절대 픽셀이다. 상태를 하나 추가하면 **손대지 않은 기존 전이까지** 검증 실패 — 9/21 실측: `waiting` 레인 col 1 에 상태 1개 + 전이 1개만 넣었는데 오류 5건이 났고 그중 3건이 손대지 않은 기존 `t-reject` 쪽이었다. 상태 추가 시 전량 재검증 필요 |
| 7/11 리뷰 연기 항목 | [reviews/2026-07-11-remediation-checklist.md](reviews/2026-07-11-remediation-checklist.md) | 각 항목에 연기 근거 기재 |