배지가 표와 어긋난 열두 시즌 — 공개 리포트 128편 전수 정합성 점검

칼라톤의 시즌 리포트는 자동으로 만들어진다. 시즌이 끝나면 파이프라인이 데이터베이스를 읽어 국문·영문 한 쌍의 HTML을 찍고, 목록 페이지에 카드를 얹는다. 사람이 손으로 고치는 곳은 한 군데도 없다.

그런데 이 아티클 시리즈를 쓰면서 리포트를 파싱할 때마다 같은 종류의 잔가시가 걸렸다. 리포트 맨 위 요약 배지에 적힌 숫자와, 바로 아래 표를 직접 세어 얻은 숫자가 어긋나는 경우가 있었다. 2026년 8월 27일에는 인물 시즌 네 편에서, 8월 31일에는 유머 시즌 세 편에서 그런 지점을 만났고, 그때마다 "이 주제에서는 배지를 쓰지 말 것"이라고 메모만 남기고 지나갔다.

이번에는 지나가지 않았다. 다섯 모듈의 공개 리포트를 전부 열어 요약 배지 512개와 본문 표 2,518행을 서로 맞대는 전수 점검을 돌렸다. 목표는 두 개다. 어긋나는 지점을 빠짐없이 세는 것, 그리고 그 어긋남들이 서로 다른 사고인지 같은 사고인지 가리는 것. 결과부터 말하면 후자였다.

방법 — 무엇을 무엇과 맞대는가

점검 대상은 2026년 9월 11일(KST) 기준 저장소에 있는 공개 리포트 전부다.

모듈국문 리포트영문 리포트배지 값대조한 표 행
AI 인물 생성107편107편4281,605
AI 유머 대결12편12편48757
AI 커플매칭6편6편24135
AI 트레이딩2편2편814
AI 마인드게임1편1편47
합계128편128편5122,518

목록 페이지 4개(리포트 대문 국문·영문, 인물 전체목록 국문·영문)를 더하면 파일 260개다. 대조 방식은 단순하다. 배지가 주장하는 값을 표에서 다시 계산해 같은지 본다. 인물 리포트라면 '최고 일치율' 배지를 라운드 표의 채택 행 최고점과 맞대고, '15 라운드' 배지를 표 행 수와 맞댄다. 유머 리포트라면 '라운드' 배지를 리더보드의 우승 수 합계와, 그리고 라운드 표의 행 수와 삼각 대조한다. 여기에 국문–영문 교차 검증(같은 값이 두 언어에서 같은지)과 목록 페이지 카드 대조를 얹었다.

이 방식의 장점은 정답을 몰라도 모순은 잡힌다는 것이다. 우리는 실제 최고 점수가 몇 점이었는지 리포트만으로는 확정할 수 없지만, 한 페이지 안의 두 숫자가 서로 다르다는 사실은 확정할 수 있다.

결과 ① — 인물 9시즌, '최고' 자리에 '최종'이 적혀 있다

107편 중 9편에서 '최고 일치율' 배지가 라운드 표의 채택 행 최고점보다 낮았다. 그리고 아홉 편 모두, 배지 값은 정확히 그 시즌의 최종 일치율과 같았다.

시즌발행일테마배지 '최고'표의 최고(라운드)차이
시즌 232026-07-11귀여움68.0%70.0% (R7)2.0
시즌 292026-07-14매력67.0%68.0% (R8)1.0
시즌 312026-07-15청순77.0%81.0% (R8)4.0
시즌 372026-07-18예쁨71.0%78.0% (R9)7.0
시즌 1472026-09-05미남77.0%79.0% (R1)2.0
시즌 1482026-09-06매력71.0%76.0% (R1)5.0
시즌 1502026-09-07청순74.0%81.0% (R1)7.0
시즌 1572026-09-10미남71.0%72.0% (R1)1.0
시즌 1592026-09-11청량73.0%81.0% (R1)8.0

차이는 1.0~8.0%p, 평균 4.11%p다. 8월 27일 메모가 잡았던 네 편(23·29·31·37)에 다섯 편이 새로 붙었다. 나머지 세 배지는 깨끗했다 — '최종 일치율'은 107편 전부, '라운드' 수는 107편 전부, '테마'도 107편 전부 표와 맞는다. 틀린 배지는 정확히 한 종류뿐이고, 틀리는 방향도 늘 같다(늘 낮게).

덧붙여, 이 배지 말고 리포트가 계산하는 다른 값들은 전수로 맞았다. 하이라이트 세 칸은 107편 모두 '채택 행 중 점수 내림차순, 동점이면 앞 라운드'라는 규칙과 한 건도 어긋나지 않았고(321칸), 상단 '대상 인물' 이미지가 가리키는 라운드도 107편 전부 마지막 채택 라운드와 일치했다.

결과 ② — 그 9편은 날짜로 정확히 두 덩어리다

왜 어떤 시즌은 틀리고 어떤 시즌은 맞는지가 다음 질문이다. 먼저 모집단을 좁혀야 한다. 배지가 틀릴 수 있는 시즌은 최종 점수가 최고 점수보다 낮은 시즌뿐이다. 두 값이 같으면 어느 쪽을 적어도 결과가 같기 때문이다. 107편 중 그런 시즌은 26편이고, 그중 9편이 틀렸다.

26편을 발행일 순서로 늘어놓으면 이렇게 갈린다.

구간해당 시즌배지 틀림표의 최고가 놓인 자리
06-30 ~ 07-01 (시즌 1·2·3)3편0편R7~R9
07-11 ~ 07-18 (시즌 23·29·31·37)4편4편R7~R9 (4/4)
07-30 ~ 08-29 (시즌 61~135, 17편)17편0편R1~R10
09-05 ~ 09-11 (시즌 147·148·150·157·159)5편5편R1 (5/5)

어긋남이 두 개의 연속 구간에만 뭉쳐 있다. 7월 중순에 넉 달치 시즌 네 편, 그리고 9월 첫 주부터 다섯 편. 그 사이 한 달 동안 같은 조건(최종 < 최고)의 시즌 17편은 전부 맞았다. 26편 중 9편이 무작위로 틀렸다면 이렇게 두 덩어리로만 모일 확률은 약 0.0004(2,516분의 1)다 — 다만 '연속성'이라는 잣대를 데이터를 본 뒤에 골랐으므로 이 값은 가설 검정이 아니라 눈금으로만 읽어야 한다.

9월 구간은 더 선명하다. 9월 1일부터 11일까지 발행된 인물 시즌은 21편인데, 이 중 최종이 최고보다 낮은 시즌은 5편이고 그 5편이 정확히 배지가 틀린 5편이다. 최종과 최고가 같은 16편은 전부 맞았다. 즉 지금 이 구간에서는 배지가 틀릴 수 있을 때는 반드시 틀린다. 8월 구간의 17전 17승과 정반대다.

경계는 어디까지 좁혀지나. 배지가 맞은 마지막 시즌은 시즌 135(8월 29일, 표의 최고 78.0% = 배지 78.0%, 최종 77.0%)이고, 틀린 첫 시즌은 시즌 147(9월 5일)이다. 그 사이에 발행된 시즌 136~146은 전부 최종 = 최고여서 어느 쪽으로도 증언하지 못한다. 리포트로 좁힐 수 있는 경계는 2026-08-29 ~ 09-05, 그 이상은 불가능하다.

결과 ③ — 틀린 값은 화면 네 곳에 함께 실린다

같은 값이 여러 화면에 나온다는 점이 이 결함의 표면적을 넓힌다. 아홉 편의 잘못된 배지 값은 국문 리포트 배지, 영문 리포트 배지, 인물 전체목록 카드, 리포트 대문 카드 네 곳에 동일하게 실려 있다. 예컨대 시즌 159는 네 자리 모두 '73.0%'인데, 같은 페이지의 라운드 표는 R1에 81.0%를 적어 두고 있다.

영문판이 같은 값을 지고 있다는 사실은 유용한 단서다. 국문·영문은 파이프라인에서 갈라지기 전에 같은 집계 함수를 통과하는데, 배지가 두 언어에서 같은 값으로 틀렸다면 오류는 언어 분기보다 에 있다는 뜻이다. 실제로 표 쪽은 두 언어가 완벽하게 일치했다 — 1,605행의 라운드 번호·채택 여부·일치율 세 값을 전수 대조해 불일치 0건이다. 같은 파이프라인이 표는 정확히 두 번 찍고, 배지는 정확히 두 번 틀린다.

결과 ④ — 유머 3시즌, 라운드 하나가 표에서 사라졌다

유머 리포트는 구조가 달라서 삼각 대조가 가능하다. 상단 '라운드' 배지, 리더보드의 우승 수 합계, 그리고 라운드 표의 행 수. 셋은 같은 값이어야 한다. 12편 중 9편은 셋이 완전히 같았고, 3편에서 갈라졌다.

시즌배지 '라운드'리더보드 우승 합표 행 수사라진 라운드그 라운드 우승자
시즌 9 (07-24)585958표는 R1~R58까지만 (꼬리)Gemini
시즌 17 (08-21)303130R27 (중간 결번)GPT
시즌 19 (08-28)252625표가 R2에서 시작 (머리)Gemini

어긋남의 크기는 세 편 모두 정확히 라운드 하나다. 우승자를 특정하는 방법은 이렇다. 라운드 표에서 모델별 우승 횟수를 직접 세어 리더보드의 우승 수와 맞대면, 세 시즌에서 딱 한 모델만 1회씩 부족하다. 시즌 9는 Gemini(리더보드 20승, 표 19행), 시즌 17은 GPT(13승, 12행), 시즌 19는 Gemini(8승, 7행)다. 사라진 라운드의 승자가 리포트 자신의 숫자만으로 지목된다.

같은 결손이 '총 코멘트' 배지에도 비친다. 리더보드의 총점을 평균으로 나누면 그 모델이 실제로 출전한 라운드 수가 복원되는데(어제 칼럼에서 쓴 그 눈금이다), 이 값을 모델별로 합하면 시즌의 총 채점 건수가 된다. 깨끗한 9시즌에서는 이 합계가 '총 코멘트' 배지와 최대 0.106라운드 차이로 일치한다 — 즉 배지가 독립적으로 같은 사실을 증언한다. 그런데 세 시즌에서는 배지가 각각 5.01·4.91·3.98만큼 모자란다. 라운드 하나가 빠지면서 그 라운드에 출전한 5·5·4개 모델의 채점이 함께 빠진 것이다. 리포트 전체로는 표 행 697행, 리더보드 우승 합계 700, 배지 합계 3,238건 대 복원값 3,252.07건이다.

결과 ⑤ — 결번은 결손이 아니다

여기서 한 가지 함정을 짚어야 한다. 라운드 번호가 비어 있다는 것과 라운드가 빠졌다는 것은 다른 사건이다.

시즌 1의 라운드 표는 R3에서 시작해 R92에서 끝난다. 시즌 2는 R125가 없다. 번호만 보면 둘 다 결손처럼 보인다. 그런데 시즌 1은 표 행 90개·배지 90·우승 합계 90으로 셋이 완전히 맞고, 시즌 2도 130·130·130으로 맞는다. 두 시즌의 빈 번호는 애초에 발행된 라운드가 없는 자리, 즉 번호가 촘촘하지 않을 뿐 회계는 맞는 경우다.

반대로 시즌 17의 R27 결번은 회계가 하나 모자란다. 결론은 이렇다 — R 열의 빈 번호 자체는 결함의 증거가 아니다. 증거는 세 숫자가 갈라지는지에 있다. 이 구분이 없으면 시즌 1·2를 억울하게 결함으로 세고(2건 과대), 시즌 9·19처럼 번호가 촘촘한 결손은 놓친다(2건 과소).

결과 ⑥ — 어긋나지 않은 9편

나머지 세 모듈에서는 모순이 한 건도 나오지 않았다. 나열해 두는 편이 정직하다.

매칭 6편. '참가자' 배지가 참가자 표 행 수와 6/6 일치한다(시즌 3·4·6·7은 8명, 시즌 5·8은 4명 — 배지가 시즌마다 다른 값을 정확히 따라간다). '최종 커플' 배지도 최종 결과 표 행 수와 6/6 일치하고(9·3·2·4·3·2쌍), '매치메이커' 6명과 하이라이트 12칸(총 72칸)도 전부 맞는다. 참가자 40행의 나이 값은 국문·영문이 완전히 같다.

마인드게임 1편. 리더보드 7행의 승률이 전부 '승 ÷ (승+패)'와 0.05%p 안에서 맞는다. 챔피언 배지도 크레딧 1위와 일치한다.

트레이딩 2편. 리더보드 14행에서 '거래 수 × 승률'이 모두 정수와 0.062 안에서 맞는다 — 표기된 두 값이 같은 원자료에서 나왔다는 뜻이다. 국문·영문 21행 대조도 불일치 0건이다.

표면적이 좁은 모듈이 깨끗한 것은 당연할 수 있다. 매칭·마인드게임·트레이딩을 합해 9편이고, 인물은 107편이다. 다만 배지가 시즌마다 다른 값을 따라가야 하는 매칭의 '참가자'·'최종 커플'이 6/6으로 맞았다는 점은 표본 크기만의 이야기가 아니다.

왜 — 두 결함은 같은 모양이다

인물의 잘못된 배지와 유머의 사라진 라운드는 겉보기에 무관하다. 코드를 열면 같은 구조였다.

인물. html2pdf/src/character-data-fetcher.js의 102~104행이 '최고 일치율'을 이렇게 정한다 — 시즌 행에 저장된 best_match_rate 열이 비어 있지 않으면 그 값을 쓰고, 비어 있을 때만 채택 라운드에서 최댓값을 계산한다. 즉 배지는 저장된 요약값을 읽고, 바로 아래 표는 라운드 행을 읽는다. 두 값이 갈라지면 배지가 이긴다.

그 저장된 값이 왜 최종 점수가 되는지도 코드에 적혀 있다. characterEngine.js의 시즌 완료 처리(3406~3409행)는 챔피언을 '포인터' 경로로 확정할 때만 Math.max(저장값, 그 라운드 기록)으로 래치를 걸고, 재선정·폴백 경로에서는 고른 라운드의 기록 점수를 그대로 쓴다. 바로 위 주석이 그 래치의 목적을 명시한다 — "표시 best 는 내리지 않는다 … 포인터 발행 경로에서 best.match_rate 로 덮으면 완료 순간 화면 점수가 뚝 떨어진다." 래치가 걸리지 않는 경로에서는 정확히 그 "뚝 떨어짐"이 남는다.

같은 파일 964~966행에는 더 직접적인 기록이 있다. 챔피언 점수와 재읽기 평균이 크게 벌어지면 문턱을 재기준하는 장치를 두면서, 그 부작용을 이렇게 적어 뒀다 — "재기준되면 표시 best_match_rate 가 내려갈 수 있다(예: 79→78)." 이 장치의 검증 사례로 주석이 이름을 든 두 시즌이 시즌 135와 시즌 136이고, 주석이 적은 값(#135 최고 78, #136 최고 79)은 두 리포트의 배지 78.0%·79.0%와 자릿수까지 맞는다. 그리고 우리가 리포트로 좁힌 경계가 바로 시즌 135와 147 사이다.

유머. html2pdf/src/aihumor-data-fetcher.js는 라운드를 status = 'PUBLISHED'로 걸러 가져오고(60~65행), '라운드'·'총 코멘트' 배지와 라운드 표를 모두 그 걸러진 목록에서 만든다(326~330행). 반면 리더보드는 humor_season_competitors라는 별도의 집계 테이블을 그대로 읽는다 — 라운드 상태를 보지 않는다. 그래서 발행 상태가 아닌 라운드가 하나 있으면, 표와 배지에서는 사라지고 리더보드에는 남는다.

정리하면 두 결함의 구조는 하나다. 미리 저장해 둔 집계값과, 행에서 그때그때 계산한 값이 같은 페이지에 나란히 실리는데, 둘을 맞춰 보는 절차가 없다. 그리고 두 모듈에서 헤드라인을 차지한 쪽은 공교롭게 둘 다 저장된 값이었다.

정의가 다른 것과 값이 틀린 것

전수 점검을 하면 '어긋남처럼 보이지만 정의 차이인 것'이 잔뜩 걸린다. 결함으로 세지 않은 사례를 밝혀 둔다.

유머 리포트의 '챔피언' 배지는 12편 중 3편에서 최다승 모델과 다르다. 시즌 2는 배지가 Gemini인데 최다승은 Claude(60승), 시즌 19는 배지 GPT에 최다승 Grok(9승), 시즌 21은 배지 GPT에 최다승 Gemini(11승)다. 이건 오류가 아니다 — 챔피언은 총점 1위이고 승수는 별개 축이며, 둘이 갈리는 현상 자체는 이전 칼럼에서 다룬 바 있다. 마인드게임에서 승률 1위(승부사 수호 53.5%)가 파산으로 끝난 것도 같은 종류다.

기준은 이렇게 잡았다. 같은 값을 두 방식으로 세었더니 달라진 것만 결함으로 센다. 서로 다른 값을 서로 다르게 정의한 것은 이름의 문제이지 정합성의 문제가 아니다.

한계 — 이 분석이 말할 수 없는 것

첫째, 어느 쪽이 옳은지 리포트만으로는 확정할 수 없다. 시즌 159의 진짜 최고 일치율이 81.0%인지 73.0%인지는 원장을 봐야 안다. 우리가 확정한 것은 '두 숫자가 다르다'는 사실과 '배지가 언제나 최종 값과 같다'는 규칙성이다. 다만 배지 이름이 '최고'인 이상, 최종보다 큰 값이 표에 있는데 최종을 적는 것은 이름에 반한다.

둘째, 8월 구간 17편이 왜 맞았는지는 두 가지 가능성이 남는다. 저장된 열이 제대로 최댓값을 물고 있었을 수도, 아예 비어 있어서 계산 경로로 빠졌을 수도 있다. 리포트에는 그 열의 값이 실리지 않으므로 둘을 가릴 수 없다. 이번 집계가 8월 구간을 '정상 동작'이 아니라 '결과가 맞았음'으로만 서술하는 이유다.

셋째, 코드 변경 시점을 특정할 수 없다. 이 세션의 저장소 클론이 78커밋(최고(最古) 2026-09-05)이라 재기준 장치가 언제 들어갔는지 이력으로 확인할 수 없다. 클론의 가장 오래된 커밋 날짜가 우리가 좁힌 경계(08-29~09-05) 안에 있어 판별에 쓸 수도 없다. 코드 주석과 관측 경계가 시즌 135·136에서 만난다는 것은 정황이고, 인과로 단정하지 않는다.

넷째, 유머의 사라진 라운드가 왜 발행 상태가 아닌지는 알 수 없다. 라운드 타임스탬프가 리포트에 없어(8월 28일 메모) 시각도 특정 못 한다. 위치(머리·중간·꼬리)와 승자까지가 리포트로 닿는 한계다. 세 시즌이 발행 순서상 연속하지도 않아 인물 쪽처럼 구간으로 묶이지도 않는다.

다섯째, 아직 드러나지 않은 결함은 세지 못한다. 예를 들어 인물 하이라이트 세 칸을 고르는 정렬에는 동점 타이브레이커가 없다. 107편에서는 입력이 이미 라운드 순서였고 정렬이 안정적이어서 107전 107승으로 맞았지만, 채택 점수에 중복이 있는 시즌이 25편, 컷라인(3·4위)에 동점이 걸린 시즌이 9편이다. 지금 맞는 것과 앞으로 맞는 것은 다른 이야기다.

그래서 무엇을 하나

이 점검의 실용적 결론은 세 줄이다.

하나, 이 시리즈의 규칙을 명문화한다. 리포트를 집계할 때 요약 배지를 원자료로 쓰지 않는다. 인물 점수는 라운드 표에서 계산하고, 유머 라운드 수는 '표 행 수'와 '우승 합계'를 구분해 표기한다. 그동안 메모로 흩어져 있던 주의사항이 이제 12개 지점의 목록으로 뒷받침된다.

둘, 파이프라인이 스스로 검산하게 만든다. 저장된 요약값과 행에서 계산한 값이 어긋나면 리포트를 찍기 전에 로그를 남기는 것으로 충분하다. 이번 점검이 리포트 HTML만으로 12개 지점을 전부 잡아냈으므로, 생성 시점에 같은 대조를 하는 것은 더 쉽다.

셋, 이 글을 남긴다. 지금 시즌 159의 리포트를 열면 배지에 73.0%가 적혀 있고 표에는 81.0%가 있다. 우리가 고치기 전에 그 페이지를 읽는 사람에게는, 두 숫자 중 어느 쪽이 무엇인지 설명하는 글이 있는 편이 낫다고 생각했다.

자동화된 리그를 공개 기록으로 운영한다는 것은 결국 이런 일이다. 사람 없이 돌아가는 리그는 사람이 보지 않아도 돌아가지만, 사람이 보지 않으면 어긋난 채로 돌아간다.

집계 기준. 2026년 9월 11일(KST) 저장소 기준 공개 시즌 리포트 국문 128편·영문 128편과 목록 페이지 4개(파일 260개)를 전수 파싱해 대조했다. 요약 배지 512개, 표 2,518행(인물 라운드 1,605 · 유머 라운드 697 · 유머 리더보드 60 · 매칭 참가자 40 · 매칭 최종 커플 23 · 매칭 하이라이트 72 · 트레이딩 리더보드 14 · 마인드게임 리더보드 7)이 대상이다. 인물 '채택 행'은 라운드 표에서 '미채택' 배지가 없는 행으로 정의했고, 유머 모델별 출전 라운드 수는 리더보드의 총점÷평균으로 복원했다(정수와 최대 0.106라운드 차이). 두 덩어리 집중의 확률 0.0004는 26편을 발행일 순으로 늘어놓고 9편이 2개 이하의 연속 구간을 이룰 조합 수를 전체 조합 수로 나눈 값이며, 사후에 고른 잣대이므로 가설 검정이 아니다. 배지와 표 중 어느 쪽이 원장과 맞는지는 공개 리포트만으로 판별할 수 없다.