diff --git a/docs/INDEX.md b/docs/INDEX.md index dabfddd..b0a1be4 100644 --- a/docs/INDEX.md +++ b/docs/INDEX.md @@ -1,5 +1,7 @@ # Project INDEX +- 2026-09-19 · `docs/diagrams/evidence-dataflow.html` 신설 — 증거 데이터 흐름(수집 → 경계 검사 → 정규화·채점 → 영속화 → 소비) 자체 완결 다이어그램. 9/18 리뷰 수정의 불변식을 그림으로 고정한다: **제공자별 풀·버킷**(RiskScorer v3), **라운드트립이 원 점수를 재현**(`_evidence_payload` ↔ `_domain_evidence_from_ui`), **host 는 저장 시 해석·복원 시 우선 반환**, 지식DB 지문 매칭이 풀링 밖 유일한 high 승격 경로. 프로덕션 LLM 경로가 영속화 payload 를 복원해 쓴다는 사실(`store_operations.py:487`, `EvidenceEnricher` 는 테스트 전용)도 반영. archify showcase 검증 통과(아티팩트 검사 9/9 · 합성 오류 0 · 경고 0), 브라우저 증거 통과(1440×900 · 2048×1320 각 Light/Dark 4컷, 오버플로 0, 최소 투영 텍스트 6.71px ≥ 6px). spec sha256 42c334cd · html sha256 268f3352 (822,655 B). CDN·원격 fetch 없음 — 폐쇄망 규칙 준수 + - 2026-09-18 · `/code-review` 15건 전수 처리 (**헤드라인 1건은 반증**). 반증: "CSE 웹 증거가 라운드트립 후 0→45점" — 리뷰가 `match` 키 없는 합성 Evidence 로 재현한 것이고, 실제 CSE 웹 결과는 `google_custom_search.py:301` 에서 `match="page"` 를 달고 오며 `matchType` 이 충실히 왕복한다(신선/복구 모두 35점). 직렬화 불변식 위반 없음. 대신 그 조사에서 **리뷰가 놓친 라이브 버그**가 나왔다: `counted_web_buckets` 가 제공자를 안 가려 Vision 이 `match="page"` 를 먼저 보고하면 **CSE 웹 히트가 점수도 사유도 없이 통째로 버려졌다**. 실수정 — **점수 v2 → v3**(풀·버킷을 제공자별로: Vision 자기 상관 뷰 4개는 60/medium 유지, Vision full + CSE page 는 45+35=80/high 복원 · 도달 불가였던 `category` 탈출구 제거 + 미등록 유명인 미탐은 의도된 트레이드임을 명시, 승격 경로는 지식DB/FINGERPRINT), **저장·복구 5종**(`_result_host` 가 자기 host 를 안 읽어 재직렬화 시 API 이름으로 퇴행하던 잠재 버그 + 점 없는 토큰 호스트 거부 · 복구가 남긴 고아 `faceCrops` 참조를 재생성/정리 · `assetMissing` 해제 경로 신설로 두 번째 소실도 감사에 남김 · 플래그+감사를 한 트랜잭션으로 · 스코어러 마이그레이션 밴드 변경에 `scoredVersion` 스탬프 + 감사 1회), **콘솔 4종**(긴 CDN 호스트가 제목을 세로 컬럼으로 만든 flex 버그 — 실측 360px 51×95px→246×19px, **1280px 9×380px→205×19px 로 폰 전용이 아니었음** · 20ch 로 잘린 호스트를 툴팁에 · `bloggerLink` 폴백 누락 · 보존 케이스 설명 표시), **PROMPT_VERSION ko-v4-structured-host**(실모델 프로브 qwen2.5:7b — grounded 3/dropped 0/verified True, 반향 없음). 테스트 +14건. **ruff clean · pytest 586 passed**. 발견된 기존 결함 1건은 미수정 기록: 로컬 ollama 가 떠 있으면 `test_http_app.py` 3건이 실호출 타임아웃으로 실패한다(기본 생성 store 가 `127.0.0.1:11434` 기본값에 붙음 — clean HEAD 에서도 동일). 커밋 b0aa4c5 / 4ba83eb / e38dad8 - 2026-08-09 · P1 2차 — **게시 호스트 복구**. P1 3번의 전제("호스트는 저장돼 있고 점수만 안 쓴다")가 틀렸다: CSE 가 실호스트를 `data["domain"]` 에 넣는데 `store_serialization.py:298` 이 `provider` 를 우선해 **저장 시점에 덮어썼고**, Vision·Naver 는 URL 만 준다. 결과적으로 LLM 프롬프트 12줄이 전부 `domain=google` 이라 "어디에 떠 있나"를 모델도 운영자도 못 봤다. `domain` 은 provider 커버리지·CSE 시그니처 매칭의 키라 재사용 불가 → **`host` 별도 키 신설**(`_result_host`: CSE displayLink 또는 URL 파싱, www 제거·소문자). 점수 무영향·`_evidence_id` 무변화(도메인 객체 해싱이라 payload 키 추가가 id 를 안 바꿈). `PROMPT_DATA_FIELDS` 의 `domain`→`host`, 콘솔 증거 카드에 monospace `host-chip`(레거시 행은 `evidenceHost()` 가 저장 URL 에서 파생 — 마이그레이션 불요). **실렌더 검증**(실 DB, 칩 82개 전부 유효 호스트, 콘솔 에러 0)에서만 드러난 결함 2건 수정: 긴 CDN 호스트가 제목을 한 글자씩 세로로 깨뜨림 → 칩 20ch 상한 + 제목 `flex:1 1 auto`, 이후 짧은 호스트까지 잘림 → 칩 `flex:0 0 auto` 고정. PROVENANCE 배점은 **보류**(카테고리가 2종뿐이라 라벨이 사실상 "geometry AND 자막"이고, "새 신호는 0점 출시 후 상관 확인" 결정과 충돌 — 한계를 `provenance.py` 상수 옆에 기록). **pytest 574 passed**(착수 전 570) diff --git a/docs/PROJECT_MAP.md b/docs/PROJECT_MAP.md index 54af8f8..09c13ed 100644 --- a/docs/PROJECT_MAP.md +++ b/docs/PROJECT_MAP.md @@ -167,6 +167,12 @@ WAL 적용됨. DB 기본 경로는 `data/copyrighter.sqlite3`(gitignore). --- +## 다이어그램 + +| 파일 | 내용 | +|---|---| +| `docs/diagrams/evidence-dataflow.html` | 증거 데이터 흐름 5단(수집·경계 검사·정규화/채점·영속화·소비). 소스는 같은 폴더의 `evidence-dataflow.dataflow.json` (archify dataflow 스키마 v1) — 그림을 고치려면 JSON 을 고쳐 `archify deliver` 로 재생성한다. 자체 완결 HTML(외부 요청 없음) | + ## 알려진 부채 | 항목 | 위치 | 비고 | diff --git a/docs/diagrams/evidence-dataflow.dataflow.json b/docs/diagrams/evidence-dataflow.dataflow.json new file mode 100644 index 0000000..fdf89c6 --- /dev/null +++ b/docs/diagrams/evidence-dataflow.dataflow.json @@ -0,0 +1,304 @@ +{ + "schema_version": 1, + "diagram_type": "dataflow", + "meta": { + "title": "증거 데이터 흐름 — 수집에서 판정까지", + "output": "docs/diagrams/evidence-dataflow.html", + "quality_profile": "showcase", + "views": [ + { + "id": "collect-boundary", + "label": "수집 경계", + "focus": [ + "vision", + "cse", + "naver", + "policy", + "normalizer" + ], + "note": "외부 응답이 Evidence 가 되기까지 통과하는 데이터 등급 경계." + }, + { + "id": "scoring", + "label": "채점", + "focus": [ + "normalizer", + "scorer", + "knowledge" + ], + "note": "제공자별 풀·버킷과, 풀링 대상이 아닌 지식DB 승격 경로." + }, + { + "id": "roundtrip", + "label": "라운드트립", + "focus": [ + "payload", + "sqlite", + "restore", + "scorer" + ], + "note": "저장과 복원이 원 점수를 재현해야 하는 고리." + } + ], + "viewBox": [ + 1080, + 500 + ] + }, + "stages": [ + { + "label": "수집" + }, + { + "label": "경계 검사" + }, + { + "label": "정규화 · 채점" + }, + { + "label": "영속화" + }, + { + "label": "소비" + } + ], + "nodes": [ + { + "id": "vision", + "type": "external", + "label": "Cloud Vision", + "sublabel": "웹 탐지 · 엔티티", + "tag": "match", + "brand": "google-cloud", + "stage": 0, + "row": 0 + }, + { + "id": "cse", + "type": "external", + "label": "Google 맞춤 검색", + "sublabel": "이미지 · 웹문서", + "tag": "match=page", + "stage": 0, + "row": 1 + }, + { + "id": "naver", + "type": "external", + "label": "네이버 검색", + "sublabel": "블로그 · 웹문서", + "tag": "promoted", + "stage": 0, + "row": 2 + }, + { + "id": "knowledge", + "type": "database", + "label": "지식DB · 주의목록", + "sublabel": "등록 이미지 지문", + "tag": "내부", + "stage": 2, + "row": 2 + }, + { + "id": "policy", + "type": "security", + "label": "데이터 등급 검사", + "sublabel": "payload 허용 목록", + "tag": "SEARCH_EVIDENCE", + "stage": 1, + "row": 1 + }, + { + "id": "normalizer", + "type": "backend", + "label": "증거 정규화", + "sublabel": "Evidence 레코드", + "tag": "source", + "stage": 2, + "row": 0 + }, + { + "id": "scorer", + "type": "backend", + "label": "RiskScorer", + "sublabel": "제공자별 풀 · 버킷", + "tag": "v3", + "stage": 2, + "row": 1 + }, + { + "id": "payload", + "type": "backend", + "label": "_evidence_payload", + "sublabel": "도메인 → payload", + "tag": "host 해석", + "stage": 3, + "row": 0 + }, + { + "id": "sqlite", + "type": "database", + "label": "evidence 테이블", + "sublabel": "payload JSON · WAL", + "tag": "cascade", + "stage": 3, + "row": 1 + }, + { + "id": "restore", + "type": "backend", + "label": "증거 복원", + "sublabel": "payload → 도메인", + "tag": "복원", + "stage": 3, + "row": 2 + }, + { + "id": "console", + "type": "frontend", + "label": "운영자 콘솔", + "sublabel": "증거 카드 · host 칩", + "tag": "판정", + "stage": 4, + "row": 0 + }, + { + "id": "llm", + "type": "backend", + "label": "LLM 증거 판독", + "sublabel": "상위 12행 화이트리스트", + "tag": "ko-v4", + "stage": 4, + "row": 1 + } + ], + "flows": [ + { + "id": "vision-payload", + "from": "vision", + "to": "policy", + "label": "웹 탐지 결과", + "classification": "외부 응답", + "route": "top-channel" + }, + { + "id": "cse-payload", + "from": "cse", + "to": "policy", + "label": "검색 결과", + "classification": "외부 응답", + "route": "straight" + }, + { + "id": "naver-payload", + "from": "naver", + "to": "policy", + "label": "블로그 · 웹문서", + "classification": "외부 응답", + "route": "bottom-channel" + }, + { + "id": "allowed-fields", + "from": "policy", + "to": "normalizer", + "label": "허용 필드만 통과", + "classification": "등급 통제", + "variant": "security" + }, + { + "id": "evidence-record", + "from": "normalizer", + "to": "scorer", + "label": "채점 입력 신호", + "classification": "주 경로", + "variant": "emphasis", + "labelDy": 36 + }, + { + "id": "fingerprint-match", + "from": "knowledge", + "to": "scorer", + "label": "지문 유사도 0.9+ → 80점", + "classification": "풀링 제외", + "labelDy": -20 + }, + { + "id": "score-band", + "from": "scorer", + "to": "payload", + "label": "점수 · 밴드 · 사유", + "classification": "주 경로", + "variant": "emphasis" + }, + { + "id": "serialize", + "from": "payload", + "to": "sqlite", + "label": "payload JSON · host", + "classification": "주 경로", + "variant": "emphasis", + "labelDy": 36 + }, + { + "id": "reload-restore", + "from": "sqlite", + "to": "restore", + "label": "리로드 · 판정 시 복원", + "classification": "읽기 경로", + "labelDy": 36 + }, + { + "id": "roundtrip-invariant", + "from": "restore", + "to": "scorer", + "label": "원 점수 재현", + "classification": "불변식", + "variant": "emphasis" + }, + { + "id": "bootstrap-read", + "from": "sqlite", + "to": "console", + "label": "bootstrap 조회", + "classification": "읽기 경로" + }, + { + "id": "evidence-reading", + "from": "restore", + "to": "llm", + "label": "증거 판독 요청", + "classification": "읽기 경로" + } + ], + "cards": [ + { + "dot": "emerald", + "title": "주 경로", + "items": [ + "외부 응답은 데이터 등급 검사를 통과한 필드만 Evidence 가 된다", + "RiskScorer v3 는 풀과 dedup 버킷을 제공자별로 나눈다 (presence 60 / entity 20)", + "점수 · 밴드 · 사유는 payload 로 직렬화되어 evidence 테이블에 남는다" + ] + }, + { + "dot": "rose", + "title": "라운드트립 불변식", + "items": [ + "_evidence_payload 로 직렬화하고 _domain_evidence_from_ui 로 복원한다", + "복원된 증거를 다시 채점하면 원 점수가 재현되어야 한다", + "result_url → url 별칭이 0점을 45점으로 바꾼 사고가 있었다", + "host 는 저장 시 해석하고 복원 시 우선 반환한다 — API 이름으로 되돌아가지 않는다" + ] + }, + { + "dot": "orange", + "title": "소비 경로", + "items": [ + "콘솔은 영속화된 payload 를 읽는다 — 호스트 칩은 20ch 로 잘리고 전체는 툴팁에 있다", + "LLM 은 복원된 증거 상위 12행만 본다 — 인용 없는 claim 은 폐기된다", + "지식DB 지문 매칭은 풀링 대상이 아니며, 라벨만으로는 닿지 않는 high 승격 경로다" + ] + } + ] +} diff --git a/docs/diagrams/evidence-dataflow.html b/docs/diagrams/evidence-dataflow.html new file mode 100644 index 0000000..f43d1e0 Binary files /dev/null and b/docs/diagrams/evidence-dataflow.html differ