Skip to content

v0.18.0 D01 — 통계 함수 10개가 같은 표에 겹쳐 쓰고 "같은 결과" 의 정의가 없던 것에서 계산 DAG·작성자 장부·정책/세대 계약·손 계산 oracle·결정성 검사까지 (2026-09-07)

  • 기간: 2026-09-07 ~ 2026-09-07 (세션 1개 08b11132, 오너 지시 "1286 진행해줘" → 분석·계획 게시 → "ㄱ")
  • 랜딩: PR #1319(Phase 1~5 한 PR, squash) — 마이그레이션·서버 함수·앱 화면·공용 서비스 변경 없음. 문서 2·기계 판독 장부 1·테스트 도구 4·테스트 2·fixture 15·기준 요약 2·장부 규칙 2줄. 선행 G02 b0e5076a·G03 e94f0d34·스텝 1-3(D03 16b8e996) 머지 확인 뒤 착수, 읽은 main beff760b(브랜치 기준 884cc181)
  • 설계서: 없음 — 분석·Phase 계획은 이슈 #1286 댓글("예상 효과·개선사항" 절 포함)
  • 정본: docs/contracts/stats-projection-dag.md(현행 10단계·표별 현재/목표 작성자·목표 DAG 6단계·작성자 14·DTO 6·화면 읽기 표·동치 판정 정의) · docs/contracts/stats-projection-policy.md(정책 9종·반올림·null/0·동률·세대 G-1~G-8·비교 제외 사유·불변식 16·근거 매핑) · 장부 supabase/contracts/stats-projection-writers.json(계약 버전 1) · 총괄 문서 2026-09-07-v0-18-0-architecture-roadmap.md §12 D01 행
  • 도구: tests/db/support/projectionWriters.mjs(schema.sql 정적 분석 — 작성 함수·열) · oracleLoader.mjs(oracle 사례를 서버 저장 문으로 적재·큐 소진·metric 읽기 SQL) · projectionSnapshot.mjs(논리 키 스냅샷·uuid 정규화·요약 sha256·diff) · projectionInvariants.mjs(불변식 SQL 12)
  • 게이트: tests/db/statsProjectionWriters.test.mjs 7건(CI 단위 잡 — 작성자 드리프트·목표 중복 0·동적 SQL 0·미등재 표 0·단계 순서) · tests/react/statsOracleTools.test.mjs +1(어휘·단계·1/0) · tests/db/statsProjectionDeterminism.test.mjs 4건(샌드박스 전용 — oracle 대조·결정성·불변식·workload 기준 요약). 로컬 npm run check 통과. pgTAP·e2e 미접촉(샌드박스는 ci:local --full --only db 로 만들어 별도 사용)
  • 버그리포트: 없음(수리 건 아님 — 드러난 것 4건은 §4 와 후속 이슈 인계로)
  • 계약: 새 계약 문서 2(위). 기존 dirty event·잡 큐·원본 불변·정책 파일(docs/policies/**) 무변경. 수치 정책 값 변경 0(P2). G02 oracle 적재기 tests/support/statsOracle.mjs 는 복합·무게 없는 세트를 허용하도록 형식 검사만 넓힘

Phase 현황

Phase내용상태
Phase 0조사(worker 10단계·작성자 지도·읽기 RPC·정책 목록·Production 실측)·계획 게시·워크트리✅ 이슈 댓글
Phase 1계산 DAG·작성자 계약 문서 · 장부 JSON(표 20) · 정적 분석 검사 7건a6aedf9e
Phase 2정책·세대 계약 문서7b420a5c
Phase 3oracle 코퍼스 사례 14건(002~015) · 입력 단계·metric 어휘 규칙 · 적재기 형식 확장47fbbd4f
Phase 4샌드박스 동치 검사(oracle 적재 대조 116/116·결정성·불변식 12·workload 1년/4년 기준 요약) · 문서 정정(e1RM 저장 1kg)f948f54c
Phase 5장부 규칙·이 기록·등록 2곳·전체 검사·PR·인계·HQ 갱신안✅ PR #1319

1. 배경

v0.18.0 통계 트랙(D01~D11)은 "세션 하나를 고치면 그 뒤 전부를 다시 쓰는" 지금 구조를 증분 계산·사용자별 worker·완전한 세대 발행으로 바꾼다(G04 실측: 매분 cron WAL 46GB/20일, 10년 이력 소진 127초). 그 전에 무엇이 "같은 결과" 인지, 어느 표를 누가 쓰는지 를 적어 두지 않으면 바꾼 뒤에 맞는지 틀린지 판정할 수 없다. D01 은 코드를 바꾸지 않고 그 기준을 만든다.

2. 문제 제기

유저 A 의 세트 하나가 지나는 길에 주인이 없었다

유저 A 가 8월 12일 스쿼트 세션의 세트 무게 하나를 고치면 서버는 잡 하나를 큐에 넣고 worker 가 통계 함수 10개를 순서대로 돌린다. 그 10개가 같은 표에 겹쳐 쓴다 — 세션×종목 합계 표는 6개 함수(첫 함수가 행을 지우고 다시 넣되 추정 1RM 열은 비워 두고, 뒤 함수 넷이 열을 나눠 채우고, 세트 삭제 트리거가 12열을 NULL 로), 기간 통계 5개(지우는 함수가 둘), 훈련 기간 4개, 종목 통계 3개. 한 행의 최종 값은 "마지막에 쓴 함수" 가 정하고, 어느 함수가 어느 열의 주인인지 적힌 곳이 없었다.

"같다" 의 정의가 없었다

표마다 updated_at·materialized_at·applied_version·무작위 id 가 논리 값과 섞여 있어 그대로 비교하면 항상 다르다. 같은 입력을 두 번 계산해 같은 결과인지 검사하는 테스트가 없었고, 기대값이 있는 테스트는 대부분 앱 출력을 박제한 것이었다(G02 가 oracle 규칙을 만들고 사례 1건만 남긴 상태).

정책 버전과 세대가 내용을 고정하지 않았다

근력 3표는 행마다 정책·곡선 버전을 갖지만 기간·달력·종목 통계·훈련 기간 6표에는 버전 열도 세대 열도 없다. 매일 도는 롤오버는 이미 발행된 세대 번호로 PR 스냅샷을 다시 써서, 같은 세대의 내용이 날짜에 따라 바뀌었다.

3. 해결 방안

원칙 (오너 결정 없음 — 전제 1~6 으로 진행)

  • 코드를 바꾸지 않는다. 계약·장부·fixture·검사만 만든다(유저 데이터·Production 무영향).
  • 수치 정책은 값 그대로. 기대값 출처는 손 계산(hand:)·정책 표(table:)·집계 실측(production:)만.
  • 장부에 "현재 작성자" 와 "목표 작성자" 를 나란히 두고, 검사는 현재 = 실제 코드(드리프트), 목표 = 표당 하나 를 잰다. 실제 이동은 D02~D11 이 한 표씩.
  • 동치 = ① oracle(맞는 답) ② 기준 스냅샷(옛 구현과 같은 답) ③ 정책 불변식(구현 무관 성질) 셋 다.

접근

대안채택이유
장부(JSON) + schema.sql 정적 분석 검사 + 문서 2 + oracle 사례 + 샌드박스 결정성 검사채택코드를 안 건드리고도 "지금 누가 쓰는가" 를 기계가 고정하고, 후속 트랙이 옮길 때마다 같은 검사가 판정한다
작성자를 지금 바로 하나로 합치는 SQL 리팩터링미채택D02·D04~D11 소유. 기준 없이 옮기면 "같은 버그" 와 "같은 답" 을 구분 못 한다(완료 조건 3)
무결성 검사 함수(validate_user_exercise_stats_integrity_engine_v1)만 확장미채택그 함수는 관측을 같은 코드로 다시 계산해 대조한다 — 구현 복사 비교. 손 계산 oracle 과 불변식이 따로 필요
결정성 검사를 CI 에 넣기미채택(지금)샌드박스 스택이 필요. PR 마다 로컬 결과 줄을 붙이는 규칙으로 보완, CI 편입은 D08/R 계열 판단

4. 적용한 내용

Phase 1 — DAG·작성자 장부 (a6aedf9e)

  • docs/contracts/stats-projection-dag.md: 유저 A 의 세트 수정 한 건이 지나는 길(§1) · worker 한 라운드의 10단계 입력·출력·잠금 표(§3) · 표별 현재 작성자 요약(§4) · 목표 DAG 6단계(관측 → 세션 합계 → 순차 파생 → 기간·달력 → 스냅샷 → 발행)·작성자 14·규칙 6·DTO 6(§5) · 화면 RPC 가 읽는 투영/원본 표(§6, A06 입력) · 세대(§7) · 확장·이전 절차(§8) · 동치 판정 정의(§9) · 후속 인계(§10).
  • supabase/contracts/stats-projection-writers.json: 표 20개(투영 16·control 3·catalog 1)마다 단계·논리 키·비교 제외 열(사유 7종)·현재 작성자(함수·insert 열·update 열·delete)·목표 작성자. currenttests/db/support/projectionWriters.mjs 가 schema.sql 을 정적 분석해 만든 것을 그대로 넣는다(손으로 고치지 않음).
  • tests/db/statsProjectionWriters.test.mjs 7건: ① 현재 작성자 = schema.sql(드리프트) ② 표마다 목표 작성자 하나 ③ 논리 키·제외 열이 실제 열 ④ 동적 SQL 0 ⑤ refresh_* 가 쓰는 user_* 표 전부 등재 ⑥ 단계 순서. CI 단위 잡에서 돈다.

Phase 2 — 정책·세대 계약 (7b420a5c)

  • docs/contracts/stats-projection-policy.md: 정책 9종의 id@version·정본·DB 고정·행에 새겨지는 곳·검사(§2, P-1~P-3) · 반올림 자리(§3) · null/0(§4) · 같은 날·동률(§5) · 세대 G-1~G-8 + control 불변식(§6) · 비교 제외 사유(§7) · 유효 무게·manual baseline·PR 근거(§8) · 불변식 16(§9) · 정책 문장 ↔ 근거 함수 ↔ 기존 테스트(pgTAP 25파일·Node 4파일)(§10).

Phase 3 — oracle 코퍼스 (47fbbd4f)

  • 사례 14건 추가(002~015): 과거 세트 수정·세션 삭제·날짜 이동·종목 교체·복합(클린+저크)·체중(풀업)·덤벨 배수·날짜 있는 수동 PR·날짜 없는 baseline·같은 날 동률·반복 실패(3회/0회)·반복수만(푸쉬업)·출석 주/월·바벨 세트 목적. 기대값 120개 전부 hand:/table:.
  • README 에 입력 단계(steps 저장·수정·삭제·수동 PR)·복합 parts·무게 없는 세트·metric 어휘 표(접두 9)·1/0·absent 규칙. tests/react/statsOracleTools.test.mjs 가 어휘·단계를 검사.

Phase 4 — 샌드박스 동치 검사 (f948f54c)

  • tests/db/statsProjectionDeterminism.test.mjs: ① 사례 15건을 save_session_v5_engine/delete_session_v5_engine/수동 PR 삽입으로 넣고 worker 로 큐를 비운 뒤 투영 표에서 metric 을 읽어 대조 — 116/116 일치(정책 함수 값 2개는 G02 도구 테스트가 대조) ② 전체 재계산 전후 스냅샷 동일(D-1) ③ 불변식 12 위반 0 ④ G04 workload history-1y(세션 260·투영 행 9,097) 적재 → 결정성 → 불변식 → 기준 요약(표별 행 수·sha256) 저장. history-4y(세션 1,043·투영 행 35,967) 도 같은 절차로 diff 0·불변식 0·기준 요약 저장(82초).
  • 기준 요약 tests/fixtures/stats-projection-baseline/{history-1y,history-4y}.json(기준 커밋 884cc181) — 계산 결과가 바뀌면 검사가 알려 주고, 의도한 변경이면 BARBELIC_WRITE_BASELINE=1 로 다시 쓰며 PR 에 이유를 적는다.

주요 결정과 근거

  • 장부의 current 는 생성물이다 — 새 작성 함수가 생기면 검사 ①이 막고, 장부를 고치려면 생성기 출력을 붙여야 한다. 손으로 쓰면 드리프트가 다시 생긴다.
  • 기준 파일은 요약(sha256)만 저장 — 전체 행 JSON 은 1년 프로필만 수 MB. 요약이면 변화 여부를 잡고, 어디가 다른지는 로컬 BARBELIC_D01_SNAPSHOT_DIR 로 두 스냅샷을 남겨 diff 한다.
  • oracle 적재는 metric 을 투영 표에서 직접 읽는다 — 화면 RPC 를 거치면 A06 의 변경이 D01 검사를 깨뜨린다. 투영의 값 자체가 계약이다.
  • 세대 번호·시각·무작위 id 는 비교에서 뺀다 — 사유 7종을 장부에 적고 검사 ③이 열 존재·사유 정의를 확인한다.

작업 중 드러난 것

  1. e1RM 은 관측·일별·상태 표 모두 1kg 정수로 저장된다(policy.json published_projection_kg). 계획 단계 문서에는 "numeric 그대로" 로 잘못 적었다가 oracle 적재 대조(3건 불일치: 111.28 → 111, 100.58 → 101, 104.59 → 105)로 잡아 정정 — 손 계산 oracle 이 첫 실행에서 문서 오류를 찾은 사례.
  2. user_exercise_records 는 화면 함수가 하나도 읽지 않는 투영(Production 3,970행). 1단계가 종목 통계를 만들 때 중간 표로 읽고 5단계가 보정까지 하지만 세션 합계 표가 같은 값을 갖는다 → D02 폐기 판단.
  3. 롤오버가 이미 발행된 applied_version 으로 PR 스냅샷을 다시 쓴다 → 세대 계약 G-3 어긋남, D11 수리.
  4. 세션 합계·기간·달력 등 6표에 세대·정책 버전 열이 없다 → "어느 정책으로 계산됐는가" 를 세대로만 판정(P-2), D11 판단.
  5. 관측 표에 3단계(상태)의 값이 되적히는 열 10개(reference_e1rm_kg·set_intensity_percent·recent_performed_1rm_kg …) → 목표 규칙 3(한 방향) 위반, D02 가 열 위치를 정한다.
  6. 카탈로그 한글 이름은 프론트 스쿼트(띄어쓰기) — oracle 사례가 이름으로 종목을 고르므로 정확히 적어야 한다(README 에 명시).
  7. 저장 문은 request_hash 가 소문자 SHA-256 이어야 한다(적재기가 처음에 uuid 로 보내 거부됨).

5. 적용 결과

항목
투영 표 작성자 장부없음표 20개 × 현재/목표 작성자·논리 키·제외 열, 드리프트 검사 7건(CI)
표당 작성자 수(현재)세션 합계 6 · 기간 5 · 훈련 기간 4 · 종목 3 · 그 외 1~3그대로(코드 미변경) — 목표 열은 표당 1, 검사 ②가 고정
"같은 결과" 정의없음논리 키·제외 열·허용 오차·random_ref 치환·스냅샷 창 anchor(DAG §9)
손 계산 oracle 사례1(기대값 6)15(기대값 120, app: 0) — 샌드박스 대조 116/116
결정성 검사없음oracle 소유자 15명 + workload 1년(투영 행 9,097) 전체 재계산 전후 diff 0 · 4년(세션 1,043·투영 행 35,967) diff 0
정책 불변식pgTAP 정책 단언(구현 복사 포함)구현 무관 불변식 12 SQL, 위반 0
옛 구현 결과 기준없음기준 요약 파일 2(1년·4년, 커밋 884cc181)
정책 문장 ↔ 근거 매핑흩어짐정책 9종 표 + 근거 함수·기존 테스트 매핑 1표
세대 규칙코드에만G-1~G-8 문장 + control 불변식(C-1 검사)

미검증: 10년 프로필 결정성(opt-in, 약 5분 — 이번에 돌리지 않음). 증분 계산과의 비교는 증분 구현이 없으므로 절차만 있다(합격 기록 없음 — 이슈 완료 조건 그대로).

6. 이번 개선으로 향상된 것

  • D02·D04~D11 이 통계 표를 옮길 때 "누가 마지막에 썼는가" 가 아니라 장부·검사가 판정한다 — 새 작성 함수는 장부 없이는 CI 를 통과하지 못한다.
  • 통계 구조를 바꾼 뒤 "같은 답인가" 를 세 가지 독립 증거(손 계산 oracle·기준 요약·불변식)로 확인할 수 있다. 첫 실행에서 문서 오류(e1RM 반올림)를 잡은 것이 그 증거다.
  • 정책 값이 어디에 고정돼 있고 어느 표에 새겨지는지, 세대가 무엇을 약속하는지가 한 문서에 있다 — 정책을 바꾸는 것과 구조를 바꾸는 것이 구분된다.

남은 것

  • 후속 인계(이슈 #1286 댓글): D02(작성자 14·DTO·records 폐기·논리 키 unique) · D04(단계별 입력 표·부수 효과 제거·S-1) · D08(잠금 단위·lease) · D11(세대 열 없는 6표·롤오버 세대·전 표 발행 게이트) · A06(화면 읽기 표).
  • 결정성 검사의 CI 편입 여부(샌드박스 필요) — HQ/R 계열 판단.