docs: 2026-07-11 multi-agent review report, remediation checklist, offline extras
- Add the 2026-07-11 multi-agent project-review report and the remediation checklist (per-item status, with deferred items and their rationale). - offline-install: document Ollama model and Playwright Chromium transfer, a hash-verified lock, --only-binary, and correct the egress note (image comparison fetches arbitrary SSRF-guarded public URLs, not only API hosts).
This commit is contained in:
parent
6ab6da3719
commit
2c607e0df6
3 changed files with 279 additions and 4 deletions
|
|
@ -13,15 +13,29 @@ carried across the air gap as pre-downloaded wheels. The build/staging machine
|
||||||
## 1. On the build machine (has internet)
|
## 1. On the build machine (has internet)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Runtime only:
|
# Runtime only (--only-binary keeps sdists out so nothing has to build on the
|
||||||
pip download -r requirements.txt -d wheelhouse/
|
# offline target; also carry pip/setuptools/wheel themselves):
|
||||||
|
pip download --only-binary=:all: -r requirements.txt -d wheelhouse/
|
||||||
|
pip download --only-binary=:all: pip setuptools wheel -d wheelhouse/
|
||||||
|
|
||||||
# Or runtime + test tooling:
|
# Or runtime + test tooling:
|
||||||
pip download -r requirements-dev.txt -d wheelhouse/
|
pip download --only-binary=:all: -r requirements-dev.txt -d wheelhouse/
|
||||||
```
|
```
|
||||||
|
|
||||||
This fills `wheelhouse/` with every wheel (including transitive deps).
|
This fills `wheelhouse/` with every wheel (including transitive deps).
|
||||||
|
|
||||||
|
### Hash-verified lock (recommended)
|
||||||
|
|
||||||
|
Pin the full transitive tree with hashes so a tampered/substituted wheel is
|
||||||
|
rejected at install time:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# On the build machine (pip-tools):
|
||||||
|
pip-compile --generate-hashes -o requirements.lock requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
Then install on the target with `--require-hashes` (step 3).
|
||||||
|
|
||||||
## 2. Transfer
|
## 2. Transfer
|
||||||
|
|
||||||
Copy both the repository and the `wheelhouse/` directory to the target via the
|
Copy both the repository and the `wheelhouse/` directory to the target via the
|
||||||
|
|
@ -46,14 +60,56 @@ python -c "import numpy, cv2, PIL; print('deps ok')"
|
||||||
python -m pytest -q # if test deps were installed
|
python -m pytest -q # if test deps were installed
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Optional: Ollama LLM model (evidence narration)
|
||||||
|
|
||||||
|
The LLM assistant is optional. If you want it on the air-gapped target, the
|
||||||
|
model weights must be carried across too — they are **not** part of the Python
|
||||||
|
wheelhouse:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build machine (has internet):
|
||||||
|
ollama pull qwen2.5:0.5b-instruct # or your pinned OLLAMA_MODEL
|
||||||
|
# Copy the model store across the air gap:
|
||||||
|
# default: ~/.ollama/models (or set OLLAMA_MODELS to a directory you transfer)
|
||||||
|
|
||||||
|
# Target machine (no internet):
|
||||||
|
export OLLAMA_MODELS=/path/to/transferred/models
|
||||||
|
ollama serve &
|
||||||
|
# Smoke test:
|
||||||
|
ollama run qwen2.5:0.5b-instruct "hello"
|
||||||
|
```
|
||||||
|
|
||||||
|
Set `OLLAMA_BASE_URL` / `OLLAMA_MODEL` in `.env`. Leave them unset to run without
|
||||||
|
the LLM (the `llm` provider then reports disabled).
|
||||||
|
|
||||||
|
## Optional: Playwright Chromium (E2E tests)
|
||||||
|
|
||||||
|
The operator-GUI E2E suite needs a Chromium binary, which Playwright normally
|
||||||
|
downloads. For the air gap, fetch it on the build machine and carry it:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build machine:
|
||||||
|
PLAYWRIGHT_BROWSERS_PATH=./pw-browsers playwright install chromium
|
||||||
|
# Transfer the pw-browsers/ directory to the target, then:
|
||||||
|
|
||||||
|
# Target machine:
|
||||||
|
export PLAYWRIGHT_BROWSERS_PATH=/path/to/pw-browsers
|
||||||
|
python -m pytest tests/operator_gui -q
|
||||||
|
```
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
- The operator GUI (`web/operator-gui/`) is fully self-contained: all
|
- The operator GUI (`web/operator-gui/`) is fully self-contained: all
|
||||||
scripts/styles are local and the Pretendard font is bundled
|
scripts/styles are local and the Pretendard font is bundled
|
||||||
(`assets/fonts/PretendardVariable.woff2`). No CDN fetch at runtime.
|
(`assets/fonts/PretendardVariable.woff2`). No CDN fetch at runtime.
|
||||||
- External **search** APIs (Google / Naver / Cloud Vision) are an allowed
|
- External **search** APIs (Google / Naver / Cloud Vision) are an allowed
|
||||||
runtime exception and require outbound access to those providers ONLY. They
|
runtime exception and require outbound access to those providers. They
|
||||||
are optional; missing credentials disable the provider (see the startup
|
are optional; missing credentials disable the provider (see the startup
|
||||||
provider-readiness log).
|
provider-readiness log).
|
||||||
|
- **Broader egress:** beyond the API hosts, the image-comparison feature also
|
||||||
|
fetches arbitrary public result-page / image / stylesheet URLs returned by
|
||||||
|
those searches (SSRF-guarded: private/loopback/CGNAT/link-local ranges are
|
||||||
|
blocked). Restricting egress to the API hosts only disables image comparison
|
||||||
|
of external results; it does not break core review.
|
||||||
- `COPYRIGHTER_AUTH_TOKEN` should be set in `.env` for any multi-operator or
|
- `COPYRIGHTER_AUTH_TOKEN` should be set in `.env` for any multi-operator or
|
||||||
networked deployment so the data routes require a shared token.
|
networked deployment so the data routes require a shared token.
|
||||||
|
|
|
||||||
112
docs/reviews/2026-07-11-project-review.md
Normal file
112
docs/reviews/2026-07-11-project-review.md
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
# Copyrighter 프로젝트 코드 리뷰 — 2026-07-11
|
||||||
|
|
||||||
|
> 다중 에이전트 리뷰(리뷰 유닛 9개 + 백로그 전수 대조 1개, 총 33 에이전트) · 신규 CRITICAL/HIGH 발견은 독립 검증자 2명의 적대적 검증(반박 시도)을 통과한 것만 채택.
|
||||||
|
> 기준 커밋: `89c1fe4` (Phase 1 HIGH remediation) · 백엔드 테스트: **361 passed** (pytest, 3m56s).
|
||||||
|
> 지난 리뷰에서 레이트리밋으로 실패했던 arch-concurrency 유닛 재실행 완료 — 이번 신규 HIGH 9건 중 4건이 이 유닛에서 나옴.
|
||||||
|
> 신규 발견: **HIGH 9 · MEDIUM ~24 · LOW ~12 · CRITICAL 0** (기존 백로그 재발견분 제외).
|
||||||
|
|
||||||
|
## 1. 총평 (Executive Summary)
|
||||||
|
|
||||||
|
**프로젝트 건강도: 주의 (Phase 1 수습은 성공, 그러나 더 깊은 계층에서 새 HIGH 9건 발견)**
|
||||||
|
|
||||||
|
2026-06-22 리뷰(66건: CRITICAL 0 / HIGH 6 / MEDIUM 43 / LOW 17) 이후 commit 89c1fe4로 Phase 1 HIGH 5건이 실제로 수정되었음을 코드 레벨에서 확인했다. 그러나 이번 심층 리뷰(동시성·직렬화·시드 경로 집중)에서 **신규 HIGH 9건(전건 검증 CONFIRMED), MEDIUM 약 24건, LOW 약 12건**이 나왔다. CRITICAL은 이번에도 0건.
|
||||||
|
|
||||||
|
지금 가장 큰 리스크 3가지:
|
||||||
|
|
||||||
|
1. **프로덕션 스코어링이 조용히 무력화되어 있음 (H6, H7)** — `_evidence_payload`/`_domain_evidence_from_ui`(src/rights_filter/server/store_serialization.py:260, 302)가 `promoted`와 `entity` 필드를 직렬화에서 누락한다. 모든 프로덕션 점수는 `_rescore_submission`의 라운드트립을 거치므로, **Naver 프로모션 근거(+40)와 Google 웹 엔티티 근거(+40)가 프로덕션에서 항상 0점**이다. GUI에는 80% 신뢰도로 표시되면서 위험도는 'low'에 머무는, 이 도구의 핵심 기능(권리 리스크 필터링)의 침묵 결함이다. 89c1fe4의 entity-bucket dedup 수정도 프로덕션 경로에서는 사실상 죽은 코드다.
|
||||||
|
2. **긴급 제공자 차단(emergency disable)이 내구성이 없음 (H2, H3)** — `_sync_provider_payloads`(store_enrichment.py:49)는 reload/수집 때마다 저장된 `enabled` 플래그를 자격증명-연결성 기본값으로 덮어쓰고, `_apply_provider_usage_delta`(store_persistence.py:238)는 검색 시작 시점에 캡처한 stale payload 전체를 되써서 진행 중이던 검색이 끝나면 차단을 되돌린다. 컴플라이언스 목적의 긴급 정지가 감사 기록 없이 무효화된다.
|
||||||
|
3. **`_write_lock` 규율이 절반만 적용됨 (H1, H4)** — 89c1fe4는 `mark_evidence_status`와 promote 경로에만 락을 넣었고, `record_decision`(sqlite_store.py:569)과 `manual_search`(store_operations.py:321)는 여전히 무락 RMW다. 결과: **운영자의 승인/반려 결정이 동시 rescore/검색에 의해 조용히 'unreviewed'로 되돌아갈 수 있으며**, 감사 로그·워치리스트와 실제 상태가 어긋난다.
|
||||||
|
|
||||||
|
전 회차 대비 평가: 지난 리뷰가 표면(엔드포인트·GUI·개별 함수) 중심이었다면, 이번엔 "전 제출건이 단일 JSON blob + ThreadingHTTPServer" 구조에서 파생되는 **lost-update 계열**과 **직렬화 라운드트립 손실 계열**이라는 두 개의 구조적 결함군이 드러났다. 개별 수정보다 패턴 단위 일괄 적용이 필요하다.
|
||||||
|
|
||||||
|
## 2. Phase-1 수정 검증 판정
|
||||||
|
|
||||||
|
**판정: 5건 모두 커밋 내용대로 존재하고 회귀 테스트 4건 통과. 단, "좁게 맞은 수정" — 리뷰 항목 기준 4 FIXED / 1 PARTIAL.**
|
||||||
|
|
||||||
|
| # | 항목 | 판정 | 근거 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 1 | `mark_evidence_status` 원자성 | **FIXED** | sqlite_store.py:601-623 — `_write_lock` + 단일 `_transaction`, conn 스레딩, 롤백 테스트 존재 |
|
||||||
|
| 2 | `promote_collection_candidates` 락+멱등성 | **FIXED** | store_operations.py:699-771 — 락, 충돌-안전 `_stable_id`, 락 하 already-promoted 가드 |
|
||||||
|
| 3 | `deactivateCorrectionEntry` / provider-retry 정직화 | **FIXED** (단서 有) | app.js:3247-3256 실제 API 호출, :3676-3684 정직한 미구현 안내. 단, 연결된 서버 엔드포인트가 비멱등이라 파생 항목 1건이 이미 비활성이면 버튼이 영구 고장 (신규 M16) |
|
||||||
|
| 4 | entity-bucket 점수 dedup | **PARTIAL** | risk_scoring.py:102-104 핵심 수정 + 테스트 2건 존재. 번들이던 LOW(카테고리 우선순위 미러링) 미적용. **더 중요하게는 H7로 인해 프로덕션 경로에서는 이 수정 자체가 작동 불능** |
|
||||||
|
| 5 | `runManualSearch` in-flight 가드 | **FIXED** (단서 有) | app.js:2384/2390/2436/2460, rerun은 :2488 void 처리. 단, 가드 발동 시 무피드백 + 폼 덮어쓰기(신규 M17), 검색 완료 시 입력창 초기화(LOW) |
|
||||||
|
|
||||||
|
핵심 교훈: 락 수정의 주석(sqlite_store.py:598-600 "다른 writer가 끼어들지 않는다")은 **락을 잡는 writer에 대해서만** 참이다. `record_decision`, `manual_search`, `set_provider_enabled`, `emergency_disable`, `collect_keyword_candidates`, `_sync_submission_provider_state`가 락 밖에 남아 수정의 보증을 우회한다.
|
||||||
|
|
||||||
|
## 3. 신규 발견 (유닛 간 dedup 완료, HIGH는 전건 2-표 검증)
|
||||||
|
|
||||||
|
### HIGH — 9건 전건 CONFIRMED
|
||||||
|
|
||||||
|
| # | 위치 | 요약 | 발견 유닛 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| H1 | sqlite_store.py:569 | `record_decision` 무락 RMW — `_rescore_submission`(store_search_candidates.py:721-731)·bulk sweep과 경합 시 운영자 결정이 'unreviewed'로 조용히 복원, 감사로그/워치리스트와 상태 불일치 | fix-verify(M), **arch-concurrency(H)**, store-integrity(M) — 3개 유닛 교차 발견 |
|
||||||
|
| H2 | store_persistence.py:238 | `_apply_provider_usage_delta`가 검색 전 캡처한 provider payload 전체를 되써서 in-flight 검색 종료 시 enable/disable(긴급 차단 포함) 되돌림 | arch-concurrency |
|
||||||
|
| H3 | store_enrichment.py:49 | `_sync_provider_payloads`가 저장된 `enabled`를 연결성 기본값으로 덮어씀 — 모든 reload/import/upload/수집이 차단을 무감사 해제, collect의 disabled 체크(store_operations.py:575-578)는 항상 무효 | arch-concurrency |
|
||||||
|
| H4 | store_operations.py:321 | `manual_search`가 수 초~수십 초 외부 검색 전에 읽은 stale payload를 :337-347에서 무락 재기록 — 검색 중 내려진 결정/근거상태 덮어씀 | arch-concurrency |
|
||||||
|
| H5 | sqlite_store.py:285 | 0바이트/손상 이미지 1장이 `analysis_runs_for_submission(...)[-1]` IndexError로 **모든 import/reload/upload를 지속 마비**(부분 커밋 잔류, 파일 수동 삭제 전까지 반복) | store-integrity |
|
||||||
|
| H6 | store_serialization.py:260 | 직렬화가 `promoted` 누락 → 프로덕션에서 Naver 검색 근거 상시 0점 (GUI엔 80% 표시, contributed=true 거짓 표기) | analysis-scoring |
|
||||||
|
| H7 | store_serialization.py:302 | 직렬화가 `entity` 누락 → Google 웹 엔티티 상시 0점, rerun 시 엔티티 기반 쿼리 생성도 불가, 89c1fe4 dedup 수정 프로덕션 무력화 | analysis-scoring |
|
||||||
|
| H8 | index.html:227-229 | 이미지 패널 버튼(맞춤/1:1/회전) 핸들러 전무 — 죽은 버튼 (합성 단계 재검증: JS 전체에 해당 id 참조 0건) | frontend |
|
||||||
|
| H9 | app.js:2943-2955 | 후보 '무시'가 로컬 splice뿐 — 서버 엔드포인트 부재(src/ 내 dismiss 0건), 모든 bootstrap 갱신 시 무시한 후보 부활 (합성 단계 재검증 완료) | frontend |
|
||||||
|
|
||||||
|
### 검증 결과 강등 (HIGH → MEDIUM)
|
||||||
|
|
||||||
|
| # | 위치 | 요약 | 판정 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| D1 | store_serialization.py:349 | skip-사유 키워드 휴리스틱이 컴플라이언스 skip을 FAILURE(+30)로 오복원 — **단, 현 wiring에서는 트리거 문자열이 도달 불가능**(compliance 플래그가 어댑터 존재와 동치, env_clients.py:99-118). 잠재 지뢰로서 MEDIUM. 수정은 H6/H7과 같은 방법(도메인 source enum 영속화)으로 함께 처리 | DOWNGRADE (1표 REFUTED, 1표 DOWNGRADE) |
|
||||||
|
|
||||||
|
### MEDIUM — 주요 항목 (유닛 판정 그대로)
|
||||||
|
|
||||||
|
**동시성/저장소**
|
||||||
|
- M1 store_operations.py:584 — `collect_keyword_candidates` 무락 전체 삭제+재적재: promote와 경합 시 404 또는 stale 'promoted' 행 부활, 어댑터 예외 시 이전 후보 전멸 *(fix-verify·arch-concurrency·store-integrity 3중 발견)*
|
||||||
|
- M2 sqlite_store.py:630/647 — `set_provider_enabled`/`emergency_disable` 무락 RMW(usage 증분 유실, 비원자 sweep) *(arch-concurrency·store-integrity)*
|
||||||
|
- M3 store_enrichment.py:56-71 — `_sync_submission_provider_state` 전 테이블 무락 sweep이 무관한 제출건의 동시 갱신까지 되돌림
|
||||||
|
- M4 sqlite_store.py:233 — seed가 `_write_lock`을 쥔 채 레코드당 외부 HTTP 수행 → 대량 import 동안 락 필요 엔드포인트 전부 무한 대기
|
||||||
|
- M5 http_app.py:387-396 — 업로드 경로 exists()/write TOCTOU: 동명 파일 동시 업로드 시 한쪽 바이트 소실
|
||||||
|
- M6 store_queue.py:92-119 — 큐 활성 전환/레거시 이관에 감사 이벤트 전무
|
||||||
|
- M7 store_schema.py:249 — CHECK 위반 레거시 행 1건이 서버 기동을 영구 차단(수복 경로 없음)
|
||||||
|
- M8 image_store.py:93 — 동일 stem 파일(a.jpg/a.png)이 한 제출건으로 병합, 한 이미지는 미심사 통과
|
||||||
|
- M9 store_queue.py:119 — 레거시 무큐 제출건이 새 큐에 입양된 뒤 prune에서 근거까지 연쇄 삭제 (심사 이력 소실 시나리오)
|
||||||
|
|
||||||
|
**HTTP 보안 (신규만)**
|
||||||
|
- M10 http_app.py:277 — 음수 Content-Length가 10MB 캡 우회 → `rfile.read(-1)` EOF까지 읽어 메모리 고갈
|
||||||
|
- M11 http_app.py:65 — 핸들러 timeout 부재 + 무제한 스레드 → slowloris로 스레드 고갈
|
||||||
|
|
||||||
|
**스코어링/프로모터**
|
||||||
|
- M12 search_result_promoter.py:17 — blog/web "no results" 근거가 가드를 빠져나가 쿼리 텍스트 매칭으로 0.8 승격 (0건 검색이 80% 신뢰도로 표시)
|
||||||
|
- M13 search_result_promoter.py:36 — 마커가 영어 11개뿐: 한국어 Naver 결과 체계적 미탐 + 'iu'/'game'/'cover' 오탐
|
||||||
|
- M14 risk_scoring.py:38-42 — promoted Naver 결과 아이템당 +40 무상한 적층 (쿼리 1건 결과 2개면 high 밴드) — H6 수정 시 즉시 프로덕션 노출되므로 동시 수정 필수
|
||||||
|
- M15 evidence_enrichment.py:58 — (인메모리 경로 한정) KB 전 항목이 쿼리 예산 잠식, 'rejected:{id}' 항목이 문자 그대로 검색어화
|
||||||
|
|
||||||
|
**프론트엔드**
|
||||||
|
- M16 app.js:3249 — deactivate 루프 비멱등·부분 실패 시 refresh 없음 → 이미 비활성 항목 1건이 버튼 영구 고장 *(fix-verify·frontend)*
|
||||||
|
- M17 app.js:2484 — rerun이 폼을 덮어쓴 뒤 무피드백 no-op
|
||||||
|
- M18~M23 — addKnowledgeEntry의 수집탭 초기화(:2879), executeSuggestedQueries 미처리 rejection(:1052), runCandidateCollection 선-클리어(:3005), 영구 잔존 apiError 배너(:234), 8개 클릭 mutation의 in-flight 가드 부재(:3185 등), `renderCorrections`의 `derivedEntries` 무가드 참조(:2030, renderAll 전체 중단 가능)
|
||||||
|
|
||||||
|
**LOW (~12건, 대표)**: 검색 완료 시 입력 wipe(app.js:2448), batch re-promote 무음 폐기(store_operations.py:713), keep-alive 커넥션 Set-Cookie 잔류(http_app.py:264), Content-Length 파싱 오류 내부 메시지 노출(:277), FK check 미수행(store_schema.py:218), face-crop 보존기한 mtime 기준(store_enrichment.py:202), 죽은 category 분기(risk_scoring.py:87), exact: 지문 비교 불능(fingerprints.py:28), 프론트 'undefined' 렌더/스테일 패널/CSS 누락 클래스 등.
|
||||||
|
|
||||||
|
*(knownBacklog 재발견분 — ensure_queue UPDATE 키 버그(store_queue.py:114), rerun 경로 무락(backlog #12), CSP/쿠키/rate-limit 등 sec-http 6건, 프론트 5건 — 은 위 표에서 제외, 4절에 반영)*
|
||||||
|
|
||||||
|
## 4. 백로그 현황 (2026-06-22 로드맵 26개 항목)
|
||||||
|
|
||||||
|
**집계: FIXED 4 / PARTIAL 3 / OPEN 20 / REGRESSED 0** (backlog-status 유닛 전수 코드 대조 결과)
|
||||||
|
|
||||||
|
- **Phase 1 (1-5)**: 1,2,3,5 FIXED / 4 PARTIAL (LOW 서브픽스 누락 + H7로 프로덕션 무력)
|
||||||
|
- **Phase 2 보안 (6-11)**: **전 6건 OPEN** — 앱셸 CSP/XFO 부재(http_app.py:314-345), 원시 토큰 쿠키·Secure 부재(:291), 토큰 엔트로피/rate-limit 부재, `UrllibJsonTransport` 무제한 read(http_json.py:39), Origin CSRF 폴백 부재, Google API 키 URL 노출(cloud_vision_web_detection.py:57)
|
||||||
|
- **Phase 3 (12-20)**: 16 PARTIAL(LLM 사유만 필터, skip 사유는 여전히 잔류 — risk_scoring.py:57-63) / 나머지 8건 OPEN (12 rerun 무락, 13 ensure_queue, 14 무음 예외, 15 lastFailure 오귀속, 17 GUI 가드, 18 dedup 핫패스 전체 스캔, 19 EXIF 무경고 폴백, 20 reasons 가변 list)
|
||||||
|
- **Phase 4 (21-24)**: 전건 OPEN — 21은 89c1fe4에서 의도적 유예된 HIGH #6(_auto_* 메가함수, store_enrichment.py:391/:557)
|
||||||
|
- **Phase 5 (25-26)**: 25 PARTIAL(롤백·dedup 테스트 2건 추가, 미디어 401/untrusted 헤더/safeUrl/SSRF/E2E 등 다수 미작성) / 26 OPEN(offline-install.md에 Ollama·Playwright·lock 해시 부재)
|
||||||
|
- **Phase 6 LOW 배치**: 전건 OPEN
|
||||||
|
|
||||||
|
## 5. 차기 스프린트 권고 (risk-per-effort 순)
|
||||||
|
|
||||||
|
1. **[최우선] 근거 직렬화 라운드트립 복구** — store_serialization.py `_evidence_payload`/`_domain_evidence_from_ui`에 `promoted`·`promotionReason`·`entity`(·`category`) 필드 추가 + 도메인 `EvidenceSource` enum 직접 영속화(키워드 휴리스틱은 레거시 폴백으로 강등). H6·H7·D1을 한 번에 해소. **store 레벨 회귀 테스트**(auto-search → `_rescore_submission` → riskScore > 0 단언) 필수. 작은 diff, 핵심 기능 복구.
|
||||||
|
2. **[동시 필수] 프로모터 정합성** — 1번을 배포하면 M12(0건 검색 0.8 승격)·M13(영어 마커)·M14(+40 무상한 적층)가 곧바로 프로덕션 점수에 노출된다. search_result_promoter.py의 no-results 구조적 매칭 + 쿼리 텍스트 스캔 제외 + NAVER_SEARCH 기여 상한/쿼리별 dedup(risk_scoring.py)을 **같은 PR**로.
|
||||||
|
3. **[한 줄 수정] seed IndexError 가드** — sqlite_store.py:285에서 빈 run 리스트 시 'failed' 밴드 제출건 생성+감사 이벤트 후 continue. 0바이트 파일 1장으로 전 import 마비되는 H5 제거.
|
||||||
|
4. **provider enabled 내구성** — `_sync_provider_payloads`가 저장된 `enabled` 보존(연결 안 될 때만 강제 off), `_apply_provider_usage_delta`는 최신 행에 델타 병합(stale payload 전체 되쓰기 금지), `_auto_*` 드라이버가 DB 플래그 확인, `set_provider_enabled`/`emergency_disable`에 `_write_lock`+단일 트랜잭션. H2·H3·M2 해소 — 긴급 정지의 신뢰성 회복.
|
||||||
|
5. **`_write_lock` 일괄 적용** — `record_decision`(H1), `manual_search`(H4, 외부 I/O는 락 밖·RMW는 락 안), `rerun_enrichment`/`run_auto_search`(backlog #12), `collect_keyword_candidates`(M1), `_sync_submission_provider_state` 필드 단위 UPDATE 전환(M3). 패턴은 `mark_evidence_status`에 이미 있어 기계적 적용. 동시에 M4(seed 락 협소화) 검토.
|
||||||
|
6. **GUI 조작 신뢰성 배치** — H8(버튼 구현 또는 제거), H9(dismiss 서버 영속화 또는 최소 클라 필터), M16(엔드포인트 멱등화 + finally refresh), M22(공용 `withInFlightGuard` 헬퍼로 8개 mutation + setDecision(backlog #17) 일괄), M23(`derivedEntries || []`). 단일 프론트 PR.
|
||||||
|
7. **HTTP DoS 즉효 수정 + Phase 2 착수** — 음수/비정수 Content-Length 거부(M10, 2줄), 핸들러 `timeout` 설정(M11, 1줄)을 즉시. 이어서 백로그 Phase 2(items 6-11)를 보안 PR 하나로 — CSP/nosniff 헤더, 쿠키 분리, 토큰 엔트로피, `UrllibJsonTransport` 응답 캡.
|
||||||
|
8. **마이그레이션/보존 안전망** — M7(제약 재구축 시 coerce/격리 후 진행), M9(레거시 이관 시 파일 존재 검증 또는 prune 면제), M8(stem 충돌 시 확장자 포함 id), M6(ensure_queue 감사 이벤트).
|
||||||
|
|
||||||
|
**핵심 메시지**: Phase 1은 제대로 수행됐지만, 이번 리뷰의 결론은 "함수 단위 수정에서 패턴 단위 수정으로 전환하라"이다. (a) 직렬화 필드 손실 1개 패턴이 HIGH 2건+MEDIUM 1건을, (b) 무락 RMW 1개 패턴이 HIGH 3건+MEDIUM 5건을 만들었다. 위 1·4·5번을 패턴 단위로 닫으면 신규 HIGH 9건 중 7건이 사라진다.
|
||||||
107
docs/reviews/2026-07-11-remediation-checklist.md
Normal file
107
docs/reviews/2026-07-11-remediation-checklist.md
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
# 2026-07-11 리뷰 리메디에이션 체크리스트
|
||||||
|
|
||||||
|
> 기준 리뷰: `docs/reviews/2026-07-11-project-review.md` (HEAD 89c1fe4)
|
||||||
|
> 신규 HIGH 9 + 유닛 추가 HIGH 3(통합/프론트) + MEDIUM ~28 + LOW ~14.
|
||||||
|
> 배치는 risk-per-effort 순. 각 배치 후 `pytest tests/rights_filter -q` 통과 확인.
|
||||||
|
|
||||||
|
## Batch 1 — 스코어링 파이프라인 복구 (최우선) ✅ 완료
|
||||||
|
직렬화 라운드트립 필드 손실 1개 패턴이 HIGH 3건을 만듦. 프로모터 3건은 배포 즉시 노출되므로 동일 배치.
|
||||||
|
- [x] H6 `store_serialization.py` — `_evidence_payload`에 `promoted`/`promotionReason` 추가, `_domain_evidence_from_ui`에서 복원
|
||||||
|
- [x] H7 `store_serialization.py` — `entity`/`category` 추가+복원
|
||||||
|
- [x] D1 `store_serialization.py` — 실제 `EvidenceSource` 영속화(`domainSource`), 키워드 휴리스틱은 레거시 폴백
|
||||||
|
- [x] M12 `search_result_promoter.py` — no-results 구조적 매칭, 마커 스캔에서 query 텍스트 제외
|
||||||
|
- [x] M13 `search_result_promoter.py` — 한국어 마커 추가, 단문 오탐 워드바운더리
|
||||||
|
- [x] M14 `risk_scoring.py` — promoted Naver 결과 query_signature 기준 dedup
|
||||||
|
- [x] 회귀 테스트 7건 (라운드트립 score>0, 2 distinct entities, skip 무점수, dedup, 프로모터)
|
||||||
|
|
||||||
|
## Batch 2 — 동시성 / _write_lock 규율 ✅ (M4/#12 제외)
|
||||||
|
무락 RMW 1개 패턴이 HIGH 3 + MEDIUM 5.
|
||||||
|
- [x] H1 `sqlite_store.py` — `record_decision` _write_lock
|
||||||
|
- [x] H2 `store_persistence.py` — `_apply_provider_usage_delta` 델타 병합(stale payload 되쓰기 금지)
|
||||||
|
- [x] H3 `store_enrichment.py` — `_sync_provider_payloads` 저장된 enabled 보존(`operatorDisabled` 플래그로 연결성/운영자 구분)
|
||||||
|
- [x] H4 `store_operations.py` — `manual_search` 네트워크 락 밖, RMW 재읽기 후 락 안
|
||||||
|
- [x] M1 `store_operations.py` — `collect_keyword_candidates` 후보 메모리 누적→락 안 clear+insert
|
||||||
|
- [x] M2 `sqlite_store.py` — `set_provider_enabled`/`emergency_disable` 락+트랜잭션
|
||||||
|
- [x] M3 `store_enrichment.py` — `_sync_submission_provider_state` 락
|
||||||
|
- [x] 회귀 테스트 2건 (operator-disable 재시드 생존, usage delta가 in-flight disable 안 되돌림)
|
||||||
|
- [ ] #12 `rerun_enrichment`/`run_auto_search` 락 — **연기**(메가함수 내 네트워크+RMW 교차, 락 유지 시 가용성 회귀; 연기된 HIGH#6 리팩터와 함께)
|
||||||
|
- [ ] M4 seed 락 협소화 — **연기**(동일 이유, 가용성 최적화)
|
||||||
|
|
||||||
|
## Batch 3 — 시드/저장소 견고성 ✅
|
||||||
|
- [x] H5 `sqlite_store.py` — 빈 analysis run 시 'failed' 밴드+감사+continue + 테스트
|
||||||
|
- [x] #13 `store_queue.py` — ensure_queue WHERE 키를 기존 행 id로 + 테스트
|
||||||
|
- [x] M6 `store_queue.py` — 큐 활성/레거시 이관 시 감사 이벤트(스팸 방지 조건부)
|
||||||
|
- [x] M5 `http_app.py` — 업로드 TOCTOU: 배타 생성(`open xb`) 재시도 루프
|
||||||
|
- [~] M8 `image_store.py` — **되돌림**: stem-충돌 접미어가 id를 형제-의존으로 만들어 판정된 submission 중복 부활 회귀(diff 리뷰 HIGH) → `id=path.stem` 복귀. 올바른 수정은 시드 단계 incumbent-id 보존 + 자산경로 마이그레이션 필요(연기)
|
||||||
|
- [ ] M7 `store_schema.py` — 제약 재구축 위반 행 coerce/격리 (미착수 — 마이그레이션 위험)
|
||||||
|
- [ ] M9 `store_queue.py` — 레거시 이관 파일 존재 검증 (연기 — 오이관 위험, 신중 설계)
|
||||||
|
|
||||||
|
## Batch 4 — 통합/아웃바운드 ✅
|
||||||
|
- [x] HIGH `cloud_vision_web_detection.py` — per-image error → HttpJsonError + 테스트 2건
|
||||||
|
- [x] HIGH `search_policy.py` — daily_limit 날짜 윈도우 리셋 + Lock
|
||||||
|
- [x] M `google_custom_search.py` — map 응답을 try 안으로
|
||||||
|
- [x] #9 `http_json.py` — 응답 크기 캡 + decode/read 예외 계약
|
||||||
|
- [x] #10 API 키 URL→헤더 — **Vision만** `X-goog-api-key`(검증됨+테스트). customsearch는 헤더 미검증(미지원 시 검색 조용히 실패)이라 `key=` 쿼리 유지(diff 리뷰 MEDIUM)
|
||||||
|
- [x] #15 `store_enrichment.py` — lastFailure 첫 실패 아이템 (3곳)
|
||||||
|
- [x] LOW `store_remote_fetch.py` — SSRF `not is_global`(CGNAT 등 특수대역 차단)
|
||||||
|
- [x] LOW `env_clients.py` — NAVER display 3종 클램프(1~100)
|
||||||
|
- [x] M `env_clients.py` — daily-limit malformed → 보수적 100(fail-open 제거) + 테스트 갱신
|
||||||
|
- [ ] M `naver_search.py` — 멀티페이지 쿼터 실제 요청 수 (미착수)
|
||||||
|
- [ ] M `store_remote_fetch.py` — SSRF DNS-rebinding TOCTOU (미착수 — 커스텀 opener 필요)
|
||||||
|
- [ ] LOW: EUC-KR 스니핑, dead code 삭제(레이아웃 테스트가 고정 → 유지)
|
||||||
|
|
||||||
|
## Batch 5 — HTTP 보안 ✅ (M10/M11/#6)
|
||||||
|
- [x] M10 음수/비정수 Content-Length 거부 + 테스트
|
||||||
|
- [x] M11 핸들러 timeout=30
|
||||||
|
- [x] #6 앱셸 CSP/X-Frame-Options/nosniff + 테스트
|
||||||
|
- [x] #8(부분) 토큰 길이<32 경고 + static 부재 경고(`__main__`)
|
||||||
|
- [ ] #7 쿠키 분리+Secure (연기 — Secure는 localhost HTTP서 쿠키 미전송→인증 붕괴; HTTPS 전제 필요)
|
||||||
|
- [ ] #8 rate limit (미착수)
|
||||||
|
- [ ] #10 Origin/Referer CSRF 폴백 (미착수)
|
||||||
|
|
||||||
|
## Batch 6 — 프론트엔드 ✅
|
||||||
|
- [x] H8 이미지 뷰어 버튼 구현 + 케이스별 리셋
|
||||||
|
- [x] H9 dismiss 서버 영속화(신규 엔드포인트) + 테스트
|
||||||
|
- [x] HIGH '자동 항목 비활성' 선택 케이스 스코프
|
||||||
|
- [x] M16 deactivate 루프 멱등화(활성만, per-entry 실패 무시, finally refresh)
|
||||||
|
- [x] #17 setDecision in-flight 가드 + rerunSelected/applyCoverage null 가드
|
||||||
|
- [x] M18 addKnowledgeEntry 수집탭 초기화 부작용 제거
|
||||||
|
- [x] M19 executeSuggestedQueries refreshFromApi try/catch
|
||||||
|
- [x] M20 runCandidateCollection 선-클리어→성공 후로
|
||||||
|
- [x] M22 apiError 배너 applyBootstrap에서 clear
|
||||||
|
- [x] M23 renderCorrections derivedEntries 가드
|
||||||
|
- [x] #11(부분) riskClass 밴드 allowlist, case-image src safeUrl
|
||||||
|
- [ ] #11 잔여 sinks(similar-strip/face-crop safeUrl, innerHTML label) (미착수)
|
||||||
|
- [ ] #24 window.prompt → 인라인 (미착수 — 규모 큼)
|
||||||
|
- [ ] M21 8개 delegated mutation 공용 in-flight 가드 (미착수)
|
||||||
|
|
||||||
|
## Batch 7 — env/config/docs ✅
|
||||||
|
- [x] `env_file.py` UTF-8-sig BOM
|
||||||
|
- [x] `env_clients.py` daily-limit fail-open 제거
|
||||||
|
- [x] `__main__`/launcher 경로 ROOT chdir + python 3.13 가드 + static/token 경고
|
||||||
|
- [x] offline-install.md — Ollama/Playwright/lock 해시/egress 정정/`--only-binary`
|
||||||
|
- [x] .gitignore — .claude/settings.local.json, .impeccable/
|
||||||
|
- [ ] `env_clients.py` LLM assistant OLLAMA 게이팅 (연기 — env={} llm 테스트 다수 의존)
|
||||||
|
- [ ] package.json 정리 (미착수)
|
||||||
|
|
||||||
|
## Batch 8 — 에러핸들링/기타 ✅ (부분)
|
||||||
|
- [x] #14 `_sync_face_crops` 무음 예외 로깅 + preprocessing EXIF 폴백 경고(#19)
|
||||||
|
- [x] #16 skip 사유 reasons 오염 제거 + 테스트
|
||||||
|
- [x] #18 dedup 핫패스 타깃 쿼리(2곳)
|
||||||
|
- [ ] #14 잔여 무음 swallow(page scrape 등) (미착수)
|
||||||
|
- [ ] #20 ScoreResult.reasons tuple (연기 — 테스트 다수 `== []` 단언 충돌)
|
||||||
|
|
||||||
|
## Batch 7 — env/config/docs
|
||||||
|
- [ ] `env_file.py:12` — UTF-8-sig BOM
|
||||||
|
- [ ] daily-limit fail-open → 보수적 기본
|
||||||
|
- [ ] `env_clients.py:239` — LLM assistant OLLAMA_* 게이팅
|
||||||
|
- [ ] `__main__.py` — 경로 ROOT 앵커링 + static 부재 경고
|
||||||
|
- [ ] offline-install.md — Ollama/Playwright/lock 해시
|
||||||
|
- [ ] python 3.13 가드, .gitignore, package.json 정리
|
||||||
|
|
||||||
|
## Batch 8 — 에러핸들링/기타 백로그
|
||||||
|
- [ ] #14 무음 예외 로깅
|
||||||
|
- [ ] #16 skip 사유 reasons 오염
|
||||||
|
- [ ] #19 EXIF/GPS PIL 폴백 경고
|
||||||
|
- [ ] #20 ScoreResult.reasons tuple
|
||||||
|
- [ ] #18 dedup 핫패스 타깃 쿼리
|
||||||
Loading…
Reference in a new issue