diff --git a/docs/INDEX.md b/docs/INDEX.md index b0a1be4..0193cf2 100644 --- a/docs/INDEX.md +++ b/docs/INDEX.md @@ -1,5 +1,7 @@ # Project INDEX +- 2026-09-19 · 다이어그램 4종으로 확장 — `system-architecture`(폐쇄망 경계·SSRF 가드 앞 호출 게이트), `operator-review-workflow`(4레인, 비상 차단 시 심사 계속 경로), `submission-lifecycle`(분석 실패·보류가 분석으로 복귀하는 두 고리)를 신설하고 기존 `evidence-dataflow` 포함 4종의 범례를 한글화했다. 렌더러 기본 범례가 "User UI / Agent logic / policy·PII" 같은 타 도메인 문구였다. 작성 중 내린 고도 판단: 아키텍처에서는 analyzer/scorer 를 한 컴포넌트로 합쳤고(세부는 dataflow 담당), 라이프사이클에서 "재분석 요청"은 decisionStatus 가 아닌 운영자 액션이라 상태에서 제외해 복귀 엣지를 하나로 줄였다. 4종 모두 archify showcase 통과(아티팩트 검사 9/9 · 합성 오류 0 · 경고 0) + visual-check 통과(1440×900 · 2048×1320 각 Light/Dark, 오버플로 0). 수신증 — dataflow html a62f0f5c / architecture f5a4b6e1 / workflow 402914a3 / lifecycle 62e934a3 + - 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 diff --git a/docs/PROJECT_MAP.md b/docs/PROJECT_MAP.md index 09c13ed..05bbd78 100644 --- a/docs/PROJECT_MAP.md +++ b/docs/PROJECT_MAP.md @@ -169,9 +169,16 @@ WAL 적용됨. DB 기본 경로는 `data/copyrighter.sqlite3`(gitignore). ## 다이어그램 -| 파일 | 내용 | -|---|---| -| `docs/diagrams/evidence-dataflow.html` | 증거 데이터 흐름 5단(수집·경계 검사·정규화/채점·영속화·소비). 소스는 같은 폴더의 `evidence-dataflow.dataflow.json` (archify dataflow 스키마 v1) — 그림을 고치려면 JSON 을 고쳐 `archify deliver` 로 재생성한다. 자체 완결 HTML(외부 요청 없음) | +소스 JSON 과 산출물 HTML 이 같은 폴더에 쌍으로 있다. **그림을 고치려면 JSON 을 고쳐 +`archify validate` → `deliver` → `visual-check` 로 재생성한다** (HTML 직접 수정 금지). +모두 자체 완결 HTML 이라 외부 요청이 없다. + +| 파일 | 내용 | 타입 | +|---|---|---| +| `evidence-dataflow.html` | 증거 데이터 흐름 5단(수집·경계 검사·정규화/채점·영속화·소비). 라운드트립 불변식이 중심 | dataflow | +| `system-architecture.html` | 시스템 구성. 폐쇄망 경계와 그 밖의 외부 검색 API, SSRF 가드 앞의 호출 게이트 | architecture | +| `operator-review-workflow.html` | 운영자 심사 4레인(제출·자동·심사·차단). 비상 차단 시 심사가 이어지는 경로 포함 | workflow v2 | +| `submission-lifecycle.html` | 제출물 상태 전이. 분석 실패·보류가 종료가 아니라 분석으로 복귀하는 두 고리 | lifecycle | ## 알려진 부채 diff --git a/docs/diagrams/evidence-dataflow.dataflow.json b/docs/diagrams/evidence-dataflow.dataflow.json index fdf89c6..7d2d236 100644 --- a/docs/diagrams/evidence-dataflow.dataflow.json +++ b/docs/diagrams/evidence-dataflow.dataflow.json @@ -43,7 +43,24 @@ "viewBox": [ 1080, 500 - ] + ], + "legend": { + "mode": "auto", + "entries": { + "emphasis": { + "label": "주 경로" + }, + "security": { + "label": "등급 통제" + }, + "database": { + "label": "저장소" + }, + "default": { + "label": "데이터 흐름" + } + } + } }, "stages": [ { diff --git a/docs/diagrams/evidence-dataflow.html b/docs/diagrams/evidence-dataflow.html index f43d1e0..e93c548 100644 Binary files a/docs/diagrams/evidence-dataflow.html and b/docs/diagrams/evidence-dataflow.html differ diff --git a/docs/diagrams/operator-review-workflow.html b/docs/diagrams/operator-review-workflow.html new file mode 100644 index 0000000..ce6912d Binary files /dev/null and b/docs/diagrams/operator-review-workflow.html differ diff --git a/docs/diagrams/operator-review-workflow.workflow.json b/docs/diagrams/operator-review-workflow.workflow.json new file mode 100644 index 0000000..3a5c0a5 --- /dev/null +++ b/docs/diagrams/operator-review-workflow.workflow.json @@ -0,0 +1,349 @@ +{ + "schema_version": 2, + "diagram_type": "workflow", + "meta": { + "title": "운영자 심사 워크플로", + "output": "docs/diagrams/operator-review-workflow.html", + "quality_profile": "showcase", + "views": [ + { + "id": "happy-path", + "label": "정상 경로", + "focus": [ + "folder", + "seed", + "analyze", + "search", + "score", + "evidence", + "decide" + ], + "note": "폴더 투입에서 판정까지 가는 주 경로." + }, + { + "id": "stop-path", + "label": "비상 차단", + "focus": [ + "search", + "stop", + "skipped", + "evidence" + ], + "note": "외부 호출이 멈췄을 때 심사가 어떻게 이어지는지." + }, + { + "id": "assist", + "label": "판독 보조", + "focus": [ + "search", + "llm", + "evidence" + ], + "note": "LLM 이 증거를 읽고 초안을 내는 보조 경로." + } + ], + "legend": { + "mode": "auto", + "entries": { + "external": { + "label": "외부 입력" + }, + "backend": { + "label": "서버 처리" + }, + "frontend": { + "label": "운영자 화면" + }, + "security": { + "label": "차단 · 정책" + }, + "database": { + "label": "저장소" + } + } + } + }, + "lanes": [ + { + "id": "intake", + "label": "제출 · 시드" + }, + { + "id": "auto", + "label": "자동 분석 · 검색" + }, + { + "id": "review", + "label": "운영자 심사" + }, + { + "id": "exception", + "label": "차단 · 보존", + "variant": "exception" + } + ], + "phases": [ + { + "id": "p-intake", + "label": "제출 · 시드", + "fromCol": 0, + "toCol": 1 + }, + { + "id": "p-auto", + "label": "자동 분석 · 검색", + "fromCol": 2, + "toCol": 3, + "variant": "emphasis" + }, + { + "id": "p-judge", + "label": "채점 · 심사 · 판정", + "fromCol": 4, + "toCol": 5 + } + ], + "groups": [ + { + "id": "g-exception", + "label": "외부 호출이 멈춘 경우", + "lane": "exception", + "fromCol": 3, + "toCol": 4, + "variant": "security" + }, + { + "id": "g-assist", + "label": "판독 보조", + "lane": "review", + "fromCol": 3, + "toCol": 3, + "variant": "dashed" + } + ], + "mainPath": [ + "folder", + "seed", + "analyze", + "search", + "score", + "evidence", + "decide" + ], + "nodes": [ + { + "id": "folder", + "lane": "intake", + "col": 0, + "type": "external", + "label": "큐 폴더 · 업로드", + "sublabel": "이미지 · manifest", + "width": 132 + }, + { + "id": "seed", + "lane": "intake", + "col": 1, + "type": "backend", + "label": "시드 · 리로드", + "sublabel": "신규 레코드만", + "tag": "판정 보존", + "width": 132 + }, + { + "id": "analyze", + "lane": "auto", + "col": 2, + "type": "backend", + "label": "내부 분석", + "sublabel": "지문 · 얼굴 · 출처", + "width": 132 + }, + { + "id": "search", + "lane": "auto", + "col": 3, + "type": "backend", + "label": "자동 검색", + "sublabel": "쿼리 생성 · 승격", + "width": 132 + }, + { + "id": "score", + "lane": "auto", + "col": 4, + "type": "backend", + "label": "위험도 산출", + "sublabel": "점수 · 밴드 · 사유", + "tag": "v3", + "width": 132 + }, + { + "id": "llm", + "lane": "review", + "col": 3, + "type": "backend", + "label": "LLM 증거 판독", + "sublabel": "인용 검증 · 12행", + "width": 132 + }, + { + "id": "evidence", + "lane": "review", + "col": 4, + "type": "frontend", + "label": "증거 검토", + "sublabel": "호스트 · 썸네일 · 링크", + "width": 132 + }, + { + "id": "decide", + "lane": "review", + "col": 5, + "type": "backend", + "label": "판정", + "sublabel": "승인 · 보류 · 반려", + "tag": "감사 이벤트", + "width": 132 + }, + { + "id": "stop", + "lane": "exception", + "col": 3, + "type": "security", + "label": "비상 차단 스위치", + "sublabel": "쿼터 · provider 토글", + "width": 132 + }, + { + "id": "skipped", + "lane": "exception", + "col": 4, + "type": "security", + "label": "검색 건너뜀", + "sublabel": "0점 · 사유만 기록", + "width": 132 + }, + { + "id": "retained", + "lane": "exception", + "col": 5, + "type": "database", + "label": "보존된 케이스", + "sublabel": "원본 파일 없음", + "tag": "assetMissing", + "width": 132 + } + ], + "edges": [ + { + "id": "e-intake", + "from": "folder", + "to": "seed", + "variant": "default" + }, + { + "id": "e-seed-analyze", + "from": "seed", + "to": "analyze", + "variant": "emphasis" + }, + { + "id": "e-analyze-search", + "from": "analyze", + "to": "search", + "variant": "default" + }, + { + "id": "e-search-score", + "from": "search", + "to": "score", + "variant": "emphasis" + }, + { + "id": "e-score-evidence", + "from": "score", + "to": "evidence", + "variant": "emphasis" + }, + { + "id": "e-evidence-decide", + "from": "evidence", + "to": "decide", + "label": "근거 확인 후", + "variant": "emphasis" + }, + { + "id": "e-search-stop", + "from": "search", + "to": "stop", + "label": "외부 호출 요청", + "variant": "security" + }, + { + "id": "e-stop-skipped", + "from": "stop", + "to": "skipped", + "label": "차단됨", + "variant": "security", + "role": "error" + }, + { + "id": "e-skipped-evidence", + "from": "skipped", + "to": "evidence", + "label": "커버리지 부족 표시", + "variant": "default", + "role": "branch" + }, + { + "id": "e-search-llm", + "from": "search", + "to": "llm", + "label": "판독 요청", + "variant": "dashed", + "role": "async" + }, + { + "id": "e-llm-evidence", + "from": "llm", + "to": "evidence", + "label": "인용된 초안", + "variant": "dashed" + }, + { + "id": "e-decide-retained", + "from": "decide", + "to": "retained", + "label": "파일 소실 시", + "variant": "default", + "role": "branch" + } + ], + "cards": [ + { + "dot": "emerald", + "title": "주 경로", + "items": [ + "리로드는 신규 레코드만 분석한다 — 기존 행은 판정과 함께 보존된다", + "자동 검색은 쿼리를 생성하고 유사도가 맞는 결과만 증거로 승격한다" + ] + }, + { + "dot": "rose", + "title": "멈췄을 때", + "items": [ + "비상 차단은 시드 · 재분석 · 얼굴 크롭 경로까지 모두 막는다", + "건너뛴 검색은 0점이며 위험 사유가 아니라 운영 상태로만 기록된다" + ] + }, + { + "dot": "cyan", + "title": "판독 보조", + "items": [ + "LLM 은 요약기가 아니라 증거 판독기다 — 인용 없는 claim 은 폐기된다", + "프롬프트 입력은 상위 12행으로 제한된다" + ] + } + ] +} diff --git a/docs/diagrams/submission-lifecycle.html b/docs/diagrams/submission-lifecycle.html new file mode 100644 index 0000000..eb1532a Binary files /dev/null and b/docs/diagrams/submission-lifecycle.html differ diff --git a/docs/diagrams/submission-lifecycle.lifecycle.json b/docs/diagrams/submission-lifecycle.lifecycle.json new file mode 100644 index 0000000..af198c8 --- /dev/null +++ b/docs/diagrams/submission-lifecycle.lifecycle.json @@ -0,0 +1,280 @@ +{ + "schema_version": 1, + "diagram_type": "lifecycle", + "meta": { + "title": "제출물 상태 전이", + "output": "docs/diagrams/submission-lifecycle.html", + "quality_profile": "showcase", + "viewBox": [ + 1080, + 630 + ], + "views": [ + { + "id": "main", + "label": "주 단계", + "focus": [ + "intake", + "analyzing", + "pending", + "decision", + "approved" + ], + "note": "접수에서 승인까지 이어지는 다섯 단계." + }, + { + "id": "recovery", + "label": "복구 경로", + "focus": [ + "analyzing", + "failed", + "held" + ], + "note": "끝나지 않고 분석으로 되돌아오는 두 고리." + }, + { + "id": "terminal", + "label": "종료 상태", + "focus": [ + "decision", + "approved", + "rejected", + "retained" + ], + "note": "되돌아오지 않는 상태와, 판정을 남긴 채 파일만 사라진 상태." + } + ], + "legend": { + "mode": "auto", + "entries": { + "start": { + "label": "시작" + }, + "active": { + "label": "진행 중" + }, + "waiting": { + "label": "대기" + }, + "decision": { + "label": "판정" + }, + "success": { + "label": "정상 종료" + }, + "failure": { + "label": "실패 · 종료" + } + } + } + }, + "lanes": [ + { + "id": "main", + "label": "제출물 단계" + }, + { + "id": "waiting", + "label": "대기 · 보강" + }, + { + "id": "exceptions", + "label": "복구 경로" + }, + { + "id": "terminal", + "label": "종료 상태" + } + ], + "states": [ + { + "id": "intake", + "type": "start", + "label": "접수", + "sublabel": "폴더 · 업로드", + "lane": "main", + "col": 0, + "step": "01", + "tag": "신규" + }, + { + "id": "analyzing", + "type": "active", + "label": "분석 중", + "sublabel": "지문 · 얼굴 · 검색", + "lane": "main", + "col": 1, + "step": "02" + }, + { + "id": "pending", + "type": "active", + "label": "심사 대기", + "sublabel": "점수 · 밴드 산출됨", + "lane": "main", + "col": 2, + "step": "03", + "tag": "unreviewed" + }, + { + "id": "decision", + "type": "decision", + "label": "판정", + "sublabel": "운영자 결정", + "lane": "main", + "col": 3, + "step": "04" + }, + { + "id": "approved", + "type": "success", + "label": "승인", + "sublabel": "사용 가능", + "lane": "main", + "col": 4, + "step": "05", + "tag": "approved" + }, + { + "id": "held", + "type": "waiting", + "label": "보류", + "sublabel": "추가 확인 필요", + "lane": "waiting", + "col": 0, + "tag": "held" + }, + { + "id": "failed", + "type": "failure", + "label": "분석 실패", + "sublabel": "provider 실패", + "lane": "exceptions", + "col": 0, + "tag": "복구 가능", + "yOffset": 78 + }, + { + "id": "rejected", + "type": "failure", + "label": "반려", + "sublabel": "사용 불가", + "lane": "terminal", + "col": 1, + "tag": "rejected" + }, + { + "id": "retained", + "type": "failure", + "label": "보존", + "sublabel": "원본 파일 없음", + "lane": "terminal", + "col": 2, + "tag": "assetMissing" + } + ], + "transitions": [ + { + "id": "t-intake", + "from": "intake", + "to": "analyzing", + "variant": "emphasis" + }, + { + "id": "t-analyzed", + "from": "analyzing", + "to": "pending", + "variant": "emphasis", + "route": "straight" + }, + { + "id": "t-review", + "from": "pending", + "to": "decision", + "variant": "emphasis" + }, + { + "id": "t-approve", + "from": "decision", + "to": "approved", + "label": "승인", + "variant": "emphasis" + }, + { + "id": "t-hold", + "from": "pending", + "to": "held", + "label": "추가 확인 필요", + "labelDy": 24 + }, + { + "id": "t-fail", + "from": "analyzing", + "to": "failed", + "label": "provider 실패", + "variant": "security", + "labelDy": -24 + }, + { + "id": "t-retry", + "from": "failed", + "to": "analyzing", + "label": "재시도", + "variant": "emphasis", + "labelDy": 6, + "channelX": 240, + "toSide": "bottom" + }, + { + "id": "t-reject", + "from": "decision", + "to": "rejected", + "label": "반려", + "variant": "security", + "labelDy": 24 + }, + { + "id": "t-retain", + "from": "rejected", + "to": "retained", + "label": "원본 파일 소실", + "variant": "dashed", + "labelDy": 60 + }, + { + "id": "t-rerun", + "from": "held", + "to": "analyzing", + "label": "재분석 요청", + "variant": "emphasis", + "channelX": 170, + "toSide": "left" + } + ], + "cards": [ + { + "dot": "emerald", + "title": "주 단계", + "items": [ + "접수된 제출물은 분석을 거쳐 심사 대기 상태에서 점수와 밴드를 갖는다", + "판정은 운영자만 내린다 — 자동 분석은 밴드까지만 만든다" + ] + }, + { + "dot": "rose", + "title": "되돌아오는 상태", + "items": [ + "분석 실패와 재분석 요청은 종료가 아니라 분석 중으로 돌아온다", + "보류는 운영자가 증거를 보강하려는 상태다 — 재분석은 기존 증거를 지우지 않는다" + ] + }, + { + "dot": "amber", + "title": "보존", + "items": [ + "판정을 남긴 케이스는 원본 파일이 사라져도 지워지지 않는다", + "파일이 돌아오면 보존 표시는 해제되고 감사에 남는다" + ] + } + ] +} diff --git a/docs/diagrams/system-architecture.architecture.json b/docs/diagrams/system-architecture.architecture.json new file mode 100644 index 0000000..dc20cec --- /dev/null +++ b/docs/diagrams/system-architecture.architecture.json @@ -0,0 +1,338 @@ +{ + "schema_version": 1, + "diagram_type": "architecture", + "meta": { + "title": "POSA Copyrighter 시스템 구성", + "output": "docs/diagrams/system-architecture.html", + "quality_profile": "showcase", + "viewBox": [ + 1080, + 500 + ], + "views": [ + { + "id": "request-path", + "label": "요청 경로", + "focus": [ + "console", + "http", + "store", + "sqlite" + ], + "note": "정적 콘솔에서 스토어와 SQLite 까지 가는 주 경로." + }, + { + "id": "analysis", + "label": "분석 · 채점", + "focus": [ + "store", + "images", + "analysis" + ], + "note": "제출 이미지에서 증거와 점수가 만들어지는 고리." + }, + { + "id": "egress", + "label": "외부 경계", + "focus": [ + "store", + "gate", + "fetch", + "providers", + "ollama" + ], + "note": "폐쇄망을 넘는 유일한 경로와 그 앞의 두 관문." + } + ] + }, + "components": [ + { + "id": "console", + "type": "frontend", + "label": "운영자 콘솔", + "sublabel": "정적 SPA · 빌드 없음", + "tag": "app-*.js 15", + "pos": [ + 36, + 170 + ], + "size": [ + 124, + 62 + ] + }, + { + "id": "throttle", + "type": "security", + "label": "인증 스로틀", + "sublabel": "10회 / 60초", + "tag": "주소별", + "pos": [ + 250, + 60 + ], + "size": [ + 124, + 62 + ] + }, + { + "id": "http", + "type": "backend", + "label": "http_app", + "sublabel": "라우팅 · 인증 · CSP", + "tag": "바디 10MB", + "pos": [ + 250, + 170 + ], + "size": [ + 124, + 62 + ] + }, + { + "id": "images", + "type": "database", + "label": "이미지 저장소", + "sublabel": "제출물 · manifest", + "pos": [ + 464, + 60 + ], + "size": [ + 124, + 62 + ] + }, + { + "id": "store", + "type": "backend", + "label": "CopyrighterStore", + "sublabel": "믹스인 8개 조립", + "tag": "판정 · 감사", + "pos": [ + 464, + 170 + ], + "size": [ + 124, + 62 + ] + }, + { + "id": "sqlite", + "type": "database", + "label": "SQLite", + "sublabel": "payload JSON · WAL", + "tag": "SCHEMA v2", + "pos": [ + 464, + 280 + ], + "size": [ + 124, + 62 + ] + }, + { + "id": "analysis", + "type": "backend", + "label": "분석 · 채점", + "sublabel": "지문 · 얼굴 · 출처", + "tag": "RiskScorer v3", + "pos": [ + 678, + 60 + ], + "size": [ + 124, + 62 + ] + }, + { + "id": "gate", + "type": "security", + "label": "외부 호출 게이트", + "sublabel": "쿼터 · 비상 차단", + "pos": [ + 678, + 280 + ], + "size": [ + 124, + 62 + ] + }, + { + "id": "ollama", + "type": "external", + "label": "로컬 LLM", + "sublabel": "ollama · qwen2.5:7b", + "tag": "선택", + "pos": [ + 892, + 170 + ], + "size": [ + 124, + 62 + ] + }, + { + "id": "fetch", + "type": "security", + "label": "store_remote_fetch", + "sublabel": "SSRF 가드", + "tag": "DNS 1회 해석", + "pos": [ + 892, + 280 + ], + "size": [ + 124, + 62 + ] + }, + { + "id": "providers", + "type": "external", + "label": "외부 검색 API", + "sublabel": "Vision · CSE · 네이버", + "pos": [ + 678, + 420 + ], + "size": [ + 124, + 62 + ] + } + ], + "boundaries": [ + { + "kind": "region", + "label": "폐쇄망 (air-gapped) — 런타임 서드파티는 이미지 처리 3종", + "wraps": [ + "console", + "throttle", + "http", + "images", + "store", + "sqlite", + "analysis", + "gate", + "ollama", + "fetch" + ] + }, + { + "kind": "security-group", + "label": "네트워크 경계", + "wraps": [ + "gate", + "fetch" + ] + } + ], + "connections": [ + { + "id": "console-api", + "from": "console", + "to": "http", + "label": "REST", + "variant": "emphasis" + }, + { + "id": "auth-throttle", + "from": "throttle", + "to": "http", + "label": "인증 시도 제한", + "variant": "security", + "labelDy": 24 + }, + { + "id": "http-store", + "from": "http", + "to": "store", + "variant": "emphasis" + }, + { + "id": "store-sqlite", + "from": "store", + "to": "sqlite", + "label": "payload JSON", + "variant": "emphasis", + "labelDy": 24 + }, + { + "id": "store-images", + "from": "store", + "to": "images" + }, + { + "id": "analysis-store", + "from": "analysis", + "to": "store", + "label": "증거 · 점수 · 밴드", + "labelDy": 24 + }, + { + "id": "store-gate", + "from": "store", + "to": "gate", + "label": "외부 호출 요청" + }, + { + "id": "gate-fetch", + "from": "gate", + "to": "fetch", + "label": "허용 시 통과", + "variant": "security" + }, + { + "id": "fetch-providers", + "from": "fetch", + "to": "providers", + "label": "HTTPS", + "variant": "security" + }, + { + "id": "store-ollama", + "from": "store", + "to": "ollama", + "label": "판독 요청", + "variant": "dashed" + } + ], + "cards": [ + { + "dot": "rose", + "title": "경계", + "items": [ + "설치·런타임에 인터넷 없음을 전제한다 — CDN · 원격 fetch · 자동업데이트 금지", + "외부로 나가는 경로는 store_remote_fetch 하나뿐이고 SSRF 가드가 앞에 있다", + "게이트는 쿼터와 비상 차단을 본다 — 시드 · 재분석 · 얼굴 크롭 경로 모두 통과한다" + ] + }, + { + "dot": "emerald", + "title": "서버", + "items": [ + "http_app 이 라우팅 · Bearer/세션 인증 · CSP · CSRF · 10MB 바디 상한을 맡는다", + "CopyrighterStore 는 믹스인 8개로 조립되고 SQLite payload JSON 에 쓴다", + "BatchAnalyzer 를 스토어가 구동하고, 증거·점수·밴드가 스토어로 돌아온다" + ] + }, + { + "dot": "cyan", + "title": "콘솔 · 의존성", + "items": [ + "콘솔은 빌드 스텝이 없는 클래식 스크립트 15개로 전역 스코프를 공유한다 — 로드 순서가 계약", + "얼굴 검출은 YuNet(ONNX)이고 가중치는 gitignore 된 models/ 에 있다", + "런타임 서드파티는 numpy · opencv-python-headless · pillow 3종", + "LLM 은 로컬 ollama 선택 사항이며 없으면 연결이 빠르게 실패한다" + ] + } + ] +} diff --git a/docs/diagrams/system-architecture.html b/docs/diagrams/system-architecture.html new file mode 100644 index 0000000..6e5a24d Binary files /dev/null and b/docs/diagrams/system-architecture.html differ