bibimbap/.atp/work-session/20260623-104307/implementation/_cross-consistency-audit.md

8.6 KiB

교차정합 감사 — W2 결합 클러스터 + W3/W4 cross-ref (설계 전용)

HIGH 편차: 1건. 동결 단일권위(W2-3) 유지됨 — jam_scores/jam_votes/jam_awards/jam_score_stats VIEW 의 평가단위·컬럼·트랙·게이트 계약을 하류 W2-4/5/6 이 재정의 없이 소비. 단 W2-1 본문이 동결 전 표현(jam_entries.id)을 그대로 남겨 동결값(game_id 자연키)과 문서상 모순(HIGH-1, 스키마 영향 없음·문서 정정 권고).

생성: 2026-06-23 / 감사자: cross-consistency audit (Read-only). 코드/설계 수정 0.


점검 항목별 verdict

1. ★평가 단위 일관성 (최고 위험) — 편차(HIGH-1, 문서 표현 한정)

  • 동결 권위(W2-3 F8/§64/§67): 평가단위 = (jam_id, game_id) 자연키 = 활성 출품작. FK 는 game_id→games·jam_id→jams 직접. 출품검증=앱계층(jam_entries 활성행).
  • 하류 정합: jam_scores(W2-3:142-143)·jam_votes(W2-3:185-186)·jam_awards(W2-3:221-222) DDL FK 전부 (jam_id, game_id). W2-4 점수입력(/jams/{jamId}/games/{gameId}/scores), W2-5 투표(castVote(jamId, gameId, voterUserId)), W2-6 시상(트랙별 gameId 랭크) 전부 game_id 단위로 일치. game_id 혼용·jam_entries.id 직접 FK 없음 → 평가단위 일관성 OK.
  • [HIGH] W2-1:43, W2-1:65 → 문제: W2-1 본문이 "평가 단위 = jam_entries.id"(§43 "jam_entries.id 를 평가 단위로 제공", §65 "평가 단위 = jam_entries.id", §196 DDL 주석 "평가 단위 = entry.id — W2-3/4/5/6 참조점")로 동결값과 정반대 식별자를 명시. W2-3 은 이 모순을 concern 1·난제1·F8 에서 명시 인지하고 game_id 자연키로 이미 해소했으나(W2-3 이 권위), W2-1 문서는 미정정 잔존. → 구현자가 W2-1 본문만 읽으면 잘못된 평가단위로 매퍼/FK 설계 가능. 권고: W2-1 §43/§65/§196 을 "평가단위 = (jam_id, game_id) 자연키, jam_entries 가 active-UNIQUE 로 1:1 대응 보장(W2-3 F8 동결)" 으로 문서 정정. 스키마 영향 0(W2-1 DDL 자체는 game_id/jam_id 컬럼을 정상 보유, ux_jam_entries_jam_game_active 도 정합) — 순수 본문 표현 drift.

2. 동결 스키마 컬럼 정합 — OK

  • W2-3 동결 VIEW 출력 컬럼 = jam_id/game_id/weighted_total/simple_total/scored_criteria/judge_count(W2-3:281-284).
  • W2-4 소비(§97/§130/§207): weightedTotal/simpleTotal/scoredCriteria/judgeCount camelCase 정합. 정렬·alias·큰따옴표 규약 일치.
  • W2-6 소비(§36/§110-113/§191): JUDGE=jam_score_stats.weighted_total, USER_RATING=game_review_stats.avg_rating+review_count>=3, POPULAR=jam_votes count — 전부 동결 컬럼명 일치. score_value numeric(10,4) 는 트랙별 값 규약만 확정(스키마 변경 0). 컬럼명 drift 없음.

3. 게이트 계약 정합 — OK (게이트 순서 LOW 1건)

  • isJudge 시그니처: W2-2 동결 JamRoleGate.isJudge(session, jamId)(W2-2:283) ↔ W2-4 호출 jamRoleGate.isJudge(session, 42)(W2-4:167) 일치.
  • 자기출품 충돌: W2-2 제공 isOwnEntry(jamId, gameId, userId)(W2-2:285, option (b) 별도 메서드) ↔ W2-4 호출 isOwnEntry(42, 777, userId)(W2-4:171, "기본 가정 (b)") 일치. 개인+팀멤버 OR 양경로 커버(W2-2 §307 SQL). enforce 시점=점수입력(W2-2 J7) ↔ W2-4 enforce 정합.
  • 평가기간 게이트(F6): W2-3 status='EVAL' AND now∈[eval_start_at,eval_end_at] → W2-4 JamEvalWindow.isOpen(§149), W2-5 컨트롤러 진입부(§148) 동일 조건 사용. 위반=422 정책 3문서 일치.
  • [LOW] W2-4:67/138 vs W2-2:179 → 문제: 점수입력 게이트 순서가 두 문서에서 상이. W2-4 = CSRF→인증→**isJudge(403)→jam→평가기간(422)→출품작→자기출품. W2-2 §179 = CSRF→로그인→평가기간(422)isJudge(403)**→자기출품. isJudge 와 평가기간의 선후가 swap. 권고: 무해(둘 다 첫 실패 지점 반환·최종 결과 동일, W2-2 §179 이 "순서는 W2-4 결정"으로 이미 양보) — 구현은 W2-4 순서 채택 명시면 충분.

4. 중복 DDL — OK

  • jam_votes CREATE TABLE 가 W2-3(§182, 소유)+W2-5(§86) 양쪽 등장. W2-5 의 것은 §79-85 에서 "참조 사본(W2-3 §3 권위, 본 설계가 정의/변경하지 않음, DDL 미수정, 신규 DDL 0)"으로 명시 인용이고 본문(컬럼/UNIQUE ux_jam_votes_jam_voter/idx_jam_votes_jam_game)이 동결과 동일 → 발산하는 2번째 정의 아님.
  • W2-4/W2-6 은 CREATE TABLE/VIEW 0건(AC 텍스트의 부재검증 토큰만). 다른 중복 CREATE 없음.

5. 권한키 정합 — OK

  • 카탈로그 = W1(GAME_JAM_MANAGE/POST_WRITE/CONTENT_MODERATE) + W4 신규 BADGE_MANAGE.
  • W3-1: TAG_MANAGE 신규키 도입 안 함CONTENT_MODERATE 재사용으로 확정(D4 §60/§483, 본문·게이트 호출부 일관). 미래 분리는 concern 으로만 이관(키 도입 0).
  • W3-3: POST_WRITE 재사용(신규키 0, §55/§69).
  • W4 BADGE_MANAGE: 신규키 1개 추가. W1 AC-T1(카탈로그 카운트 3) 깨짐을 명시(W4 §12 concern·§455 AC-T5·§405 — "W1 AC-T1 을 4로 재측정 필요"). PermissionCatalogVerifier values() 자동 시드라 enum +1 만으로 동기. 누락 없음.

6. cross-W 참조 — OK (1건 전제 정정)

  • W3-4 배너→잼검색: W3-1 동결 GET /games/search?jam={jamSlug}(§65/§234) ↔ W3-4 §126-130 동일 라우트 앵커(+ W3-1 미확정 시 /jams/{slug} 폴백 우선순위 확정). 일치.
  • W3-3 POST_WRITE enforcement: PermissionGate.has(session, POST_WRITE.name()) 연결(§55/§325). OK.
  • W2-6 유저평점 트랙 = game_review_stats.avg_rating(W2-6 §37/§111) — 단방향 읽기(write 0, AC-T2). OK.
  • 정정(편차 아님): 감사 전제 "W2-6 유저평점 트랙 = W4 와 동일 VIEW 소비"는 부정확. W4 REVIEWER 배지는 game_review_stats 가 아니라 reputation_events(리뷰작성 이벤트 누적 임계, W4 §54/§263)를 소비. 둘은 독립 메커니즘(공유 VIEW 아님) → 발산 위험 없음(같은 VIEW 를 다르게 재정의하는 상황이 애초에 없음). 정합 무관.

7. 보안 존재성 — OK (전부 실재·비자명)

  • W2-2 잼-스코프 게이트: JamRoleGate(jam_judges 조회, 전역 PermissionGate 와 별도 축). isOwnEntry 개인+팀멤버 OR-EXISTS(§307). 전역 RBAC 무변경 단언(AC-T7). 실재·비자명.
  • W3-3 SSRF(SsrfSafeFetcher): 9항목 체크리스트 실재(§273-284 — scheme allowlist / host resolve / 모든 resolved IP 공인검증(사설·루프백·링크로컬·메타데이터 169.254.169.254) / connect / 연결소켓 peer-IP 재검증(rebinding) / redirect 매홉 재검증 / size cap / timeout / Content-Type). DNS-pin TOCTOU 한계까지 concern 명시. 비자명.
  • W3-5 zip-slip(canonical+심링크): toRealPath() 경계 재검증 + assertParentNotSymlink(부모경로 walk+isSymbolicLink) + 쓰기후 toRealPath().startsWith 재검증 + 엔트리명 사전거부(절대/UNC/백슬래시/NUL/드라이브/../길이/깊이). ZipEntry mode-bit 직접파싱은 근거와 함께 기각(over-eng). 비자명.

부차 관찰 (편차 아님 — 동결 허용범위 내)

  • [LOW] W2-5 vote mapper 시그니처 drift: W2-3 계약(§371-374)은 castVote/updateVote/hasVoted/countByGame, W2-5 구현(§231-242)은 castVote/updateVote/deleteVote/findVotedGameId/countByJam/listCountsByJam. W2-3 이 해당 시그니처를 "계약 골격(inflate 마킹, 구현 재확인)"으로 명시했고 countByGame ... (또는 listCounts 집계)로 listCounts 변형을 예고 → 동결 허용범위 내. W2-6 POPULAR 소스(listCountsByJam)와 W2-5 제공이 정합. castVote 인자순서(jamId,gameId,voterUserId)는 W2-3↔W2-5 완전 일치.
  • [LOW] 점수입력 경로 표현차: W2-2 §230 의사코드는 POST /jams/{slug}/scores(예시), W2-4 실제 계약은 POST /jams/{jamId}/games/{gameId}/scores. W2-2 시퀀스는 게이트 소비점 예시일 뿐 경로 권위 아님 → 무해.

전체 verdict

  • HIGH 1(W2-1 본문 평가단위 표현 정정 — 스키마/계약 영향 0, 문서 drift).
  • LOW 3(게이트 순서 표현차·vote mapper 시그니처 골격 변형·점수입력 경로 예시차 — 전부 동결 허용범위/무해).
  • 동결 단일권위(W2-3) 유지·하류 소비 정합 — PASS. jam_scores/jam_votes/jam_awards/jam_score_stats 의 평가단위·컬럼·트랙enum·게이트·단방향 계약을 W2-4/5/6 이 재정의 없이 소비. 중복 DDL·권한키 silent 추가·cross-W 라우트 불일치 없음.