감사에서 나온 결론은 "그림은 지금 정확하지만 정확함을 지킬 장치가 없다" 였다. 코드 유래 리터럴 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
215 lines
15 KiB
Markdown
215 lines
15 KiB
Markdown
# 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 # 브라우저 없는 호스트
|
||
```
|
||
|
||
`tests/test_diagram_sync.py` 가 `manifest.json` 의 sha256 으로 이 계약을 강제한다
|
||
(브라우저·Node·archify 없이 도는 순수 파일 비교). HTML 을 직접 고치거나 spec 만
|
||
고치고 재생성을 잊으면 `verify.ps1` 에서 막힌다. 두 시나리오 모두 실증 확인됨.
|
||
|
||
**문서 재생성 전용 요구사항** (런타임 아님, 폐쇄망 타깃에는 불필요):
|
||
Node 18+ 와 archify 스킬(`$env:ARCHIFY_HOME`, 기본값 `~/.claude/skills/archify`),
|
||
visual-check 단계에만 Chrome/Chromium. 산출 당시 generator 는 `archify 2.17.0-dev.1`
|
||
이고 `manifest.json` 에 기록된다 — 스킬 버전이 바뀌면 같은 spec 에서도 바이트가
|
||
달라지므로, 해시가 깨졌을 때 코드 변경과 툴 변경을 구분하는 근거가 된다.
|
||
|
||
모두 자체 완결 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` 로 돌리면 통과). 폐쇄망 프로젝트의 테스트가 외부 서비스의 **부재**에 의존하는 상태 |
|
||
| 다이어그램에 한글 폰트 미번들 | `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 으로는 못 고친다(레이아웃 폭은 전각으로 측정되므로 깨짐이 아니라 글자 문제). 고객 인수물로 확정되면 blocker |
|
||
| 뷰어 UI 가 영어 고정 | `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 하드 실패(현재 `store_remote_fetch` 18자 → 여유 2글자). workflow spec 은 기하 하드코딩 0개이므로 같은 저장소에 더 나은 패턴이 있다 |
|
||
| lifecycle 기하 손튜닝 8개소 | `docs/diagrams/submission-lifecycle.lifecycle.json` | `labelDy`×5·`channelX`×2·`yOffset`×1 이 현재 배치 전제의 절대 픽셀이다. 상태를 하나 추가하면 **손대지 않은 기존 전이까지** 검증 실패(실증: 오류 5건 중 4건이 기존 `t-reject`). 상태 추가 시 전량 재검증 필요 |
|
||
| 7/11 리뷰 연기 항목 | [reviews/2026-07-11-remediation-checklist.md](reviews/2026-07-11-remediation-checklist.md) | 각 항목에 연기 근거 기재 |
|