세트 난이도 버튼을 RPE 체계로 완전 편입 — 5단계·옛 난이도를 지우고 RPE 숫자 하나로 (2026-09-04)
- 기간: 2026-09-04 ~ 2026-09-04 (세션 3개 —
a4f60b93(분석·계획·정책 문서 v3) →ea2f7b27(Phase 1~4 구현) →1371d793(실데이터 드라이런·main(#1236) 합류·랜딩), 오너 지시 "RPE로 바뀐지가 언젠데 아직도 난이도 버튼이라고… 완전히 RPE 체계로 편입" / "RPE는 0~10 사이 숫자, 카드는 숏컷, 땜질 말고 근본 해결" / "0은 의미 없으니 1.0~10.0 유지") - 랜딩: PR #1260(Phase 1~4 + 랜딩,
ca232ee5) — 마이그레이션 2본20260910300000_user_fact_history_and_protection_v1 · 20260911000000_rpe_single_scale_v1(보호 층 재발행 + 실사용자 세트 변환 108+119·전량 재계산), Vercel 배포(앱 화면 변경). Production 적용은 릴리스 PR #1227(v0.17.0) 레인 — #1202·#1241·#1215·#1236과 함께 - 설계서: 이슈 #1237 본문(계획 v2, "예상 효과·개선사항" 절 포함) — artifact 없음
- 정본:
docs/data/rpe-performed-intensity.md§1(RPE 숫자 하나가 유일한 값)·§3(퇴역 규칙), 정책docs/policies/e1rm/v3/policy.json(3.0.0)·max-reps/v2/policy.json(2.0.0)·set-purpose/v3/policy.json(3.0.0), 앱src/react/barbelicCopy.ts(RPE_SCALE·RPE_SHORTCUTS·normalizeRpe·rpeFromSetRecord),src/react/ui/shared/setSemantics.ts(rpeDisplay·restMinutes), 서버perceived_rpe_from_set_payload_v1(한 릴리스 호환) - 도구: 함수 재발행 파이프라인(세션 scratchpad
xform.mjs·assemble.mjs— 샌드박스pg_get_functiondef원문에 정적 치환, 레포 밖),npm run db:loss-audit(#1236),npm run ci:local - 게이트: pgTAP
supabase/tests/database/rpe_single_scale_v1.test.sql(46단언) + 8파일 갱신(e1rm_failure_evidence_v2·max_rep_estimation_policy_v1(55단언)·set_purpose_policy·effective_load_stats_v1·record_metric_max_reps_v1·set_score_recent_performed_v1·set_score_report_bands_v1·atomic_recording_integrity_repair), 마이그레이션 자가 검증 ⑧(함수 본문effort_level·difficulty0),npm run check:policies(3.0.0 표식), 단위 테스트 26파일 갱신 +perceivedRpeRoundtrip재작성. 로컬 pgTAP:ci:local full104파일/1776 assert(main 합류 뒤) · e2e 5묶음 72/72(합류 전30251bee) - 버그리포트:
bug-report/bug-076-20260904.md - 계약:
docs/data/rpe-performed-intensity.md§1·§3·§6,docs/data/app-screen-rpc-contract.md(+ko) 집계 키rpe_counts(6칸)·세트 필드,docs/contracts/workout-screen-props.mdDraftSet.perceivedRpe,supabase/contracts/user-fact-columns.json(5단계·옛 난이도 컬럼 제거)
Phase 현황
| Phase | 내용 | 상태 |
|---|---|---|
| Phase 0 | 용어 확정 — "난이도 버튼·체감 난이도·5단계"는 금지어, 메모리 용어표 수정 | ✅ (세션 a4f60b93) |
| Phase 1 | 서버 — 정책 3종(e1rm 3.0.0·max-reps 2.0.0·set-purpose 3.0.0), 추정기 v2·분류기 v3, 세트 변환(D2·D3) → 컬럼 삭제, 관측 perceived_rpe, 집계 6칸, 함수 25개 재발행, 전량 재계산 + 무결성 검사 | ✅ PR #1260 |
| Phase 2 | 앱 자료형·저장/읽기 어댑터 — 세트 RPE 필드 6 → 1, 저장 payload 키 1, 6칸 읽기 모델, 옛 초안 +5 변환 | ✅ PR #1260 |
| Phase 3 | 화면 — 카드 "SET N RPE" 숫자 입력칸 + 단축 버튼, 세트 수정 모달, 칩 "@ RPE 8.5", 데스크톱 표 열 "RPE", 도넛 6칸/모바일 4칸 통일, RPE 가이드 5 이하 행 | ✅ PR #1260 |
| Phase 4 | 문서·기록 — 계약 15개 문서, 작업 기록, 버그리포트 | ✅ PR #1260 + docs PR #1261 |
| 랜딩 | 실데이터 사본 드라이런(유실 감사·오너 추정 1RM 전→후 표), #1202 검사 누락 수리, main(#1236) 합류·보호 층 재발행, 랜딩 잠금·CI 1회·머지 | ✅ PR #1260 (세션 1371d793) |
1. 배경
7월 말(07-27~30)에 세트 완료 카드의 표기를 RPE로 바꿨지만 "저장값은 불변, 표기만 매핑"으로 처리해 7월의 5단계 한글 키(아주 쉬웠음·쉬웠음·적당함·빡셌음·겨우함)가 코드의 정체성으로 남았다. 8/29(#885)에 RPE 저장 열(perceived_rpe)을 더했지만 계약을 "5단계 번호(effort_level)는 유도 정본으로 유지, 추정·분류·통계 스택 무변경"으로 잡아 RPE는 옆에 붙은 열이 됐다. 9/4 오너가 #1235 세션 보고의 "난이도 버튼" 문장을 지적하면서 층별로 확인했다: 세트마다 5단계 번호와 RPE 두 벌 저장, 옛 세트 108개는 RPE 칸이 비어 있고, 6~7월 옛 난이도 119세트는 화면에서 RPE 7~10처럼 보였으며, 데스크톱 리포트 도넛은 7월 이름표를 보여줬다.
2. 문제 제기
세트의 힘든 정도를 나타내는 값이 세 벌이었다 — 서버·통계는 5단계 번호를, 화면은 RPE를 읽었다
유저 A가 "어려움(RPE 8)"을 누르면 앱은 한글 키 "적당함"·번호 3·RPE 8을 함께 만들고, 서버 정책(근력 추정 2.0.0·최대 반복수 1.0.0·세트 목적 2.0.0)은 번호만 읽었다(RIR 표 1→5/2→3.5/3→2/4→1/5→0). RPE 8.5를 넣어도 추정에는 8과 같은 값으로 접혔고, 5.5나 4는 넣을 방법이 없었다. Production(2026-09-04): 세트 16,628 / 5단계 152 / RPE 44 / 5단계만 108 / 옛 난이도만 119.
화면마다 RPE 칸과 이름표가 달랐다
데스크톱 리포트 도넛 라벨 = 7월 5단계 이름표. 하루 상세 4칸(6/7/8/9+10)과 리포트 4칸(0/6/7+8/9+10)이 같은 서버 숫자를 다른 칸에 넣었다. 옛 난이도(1~10)는 한글 키가 겹쳐 세트 줄에 "@ RPE 7~10"으로 보였고 달력 월 요약은 그 컬럼을 RPE처럼 읽었다.
문서·보고도 섞여 있었다
"체감 난이도 / 난이도 버튼 / 난이도 5"가 문서 6개·소스 27곳에 살아 있었고, 메모리 용어표가 "난이도 버튼"을 정식 용어로 적어 보고마다 새어 나왔다.
3. 해결 방안
원칙 (오너 결정 2026-09-04)
- D1 RPE 숫자 단일 체계 — "RPE는 이론적으로 0~10 사이의 숫자(소수 포함)이니 그 필드로 확장해야 하고, 카드는 그 값을 빠르게 입력해 주는 숏컷 정도로 생각해야 한다(정해진 카테고리 값이 아니다). 땜질하지 말고 가장 근본적인 해결책을 만들어라." → 5단계 체계·옛 난이도 컬럼 물리 삭제, 통계는 RPE에서 직접. 이어서 "0은 의미가 없고 계산 오류 위험" → 저장 범위 1.0~10.0 유지.
- D2 RPE 칸이 빈 옛 세트 108개 = 5단계 + 5. D3 6~7월 옛 난이도 119세트 = 화면대로 RPE 7·8·9·10 변환(3 이하→7, 6 이하→8, 8 이하→9, 그 밖→10), 원값은
raw_payload.legacy_difficulty. D4 통계 경계 RPE 6.0(이상 닫힌 구간·medium, 미만 하한만·low), 세트 목적 7.0. D5 데스크톱 6칸(6 미만·6·7·8·9·10), 모바일 4칸(6 미만·6·7–8·9–10), 칸은 RPE 내림.
접근
| 안 | 내용 | 판단 |
|---|---|---|
| 라벨·칸만 고침 | 도넛 이름표와 4칸 규칙만 통일 | 기각 — 서버·정책이 5단계라 다음 화면에서 재발 |
| 5단계를 서버 안에서만 유지(계획 v1 D1-a) | 앱은 RPE만, 서버는 RPE→5단계 유도 후 종전 정책 | 기각(오너) — 반값·6 미만이 추정에 반영되지 않고 두 벌 저장이 계속 |
| RPE 단일 체계(계획 v2) | 컬럼 삭제 + 정책 major 3건 + 전량 재계산 + 6칸 집계 + 카드 숫자 입력 | 채택 — 값이 하나라 어긋날 자리가 없다. 비용은 Phase 4개로 흡수 |
랜딩 순서: #1215(운동 기록 계층 리모델링)가 같은 함수 17개와 앱 자료형을 바꾸며 먼저 랜딩 중이었으므로 이 트랙은 #1215 위에 쌓고, #1215·#1241이 main에 들어간 뒤 main 위로 옮겨 함수 원문을 다시 뽑았다.
4. 적용한 내용
Phase 1 — 서버 (20260911000000_rpe_single_scale_v1 + 보호 층 재발행 20260910300000_user_fact_history_and_protection_v1)
- 정책 테이블:
effort_scale_version→rpe_scale_version(옛 행은 null + JSON에 옛 눈금 보존), e1rm 3.0.0·max-reps 2.0.0·set-purpose 3.0.0 활성(행 표기는 베이스라인 덤프 꼴 — 계약 테스트 시드 파서가 읽는다). - 추정기
calculate_strength_observation_v2(RIR = 10 − RPE, ≥ 6.0 닫힘·medium, < 6.0 열림·low, 1회 @ 10.0 = high, 미입력·실패 규칙 불변)·calculate_max_rep_observation_v2(추정 최대 = 횟수 + (10 − RPE))·classify_set_purpose_v3(13회 이상 RPE 7.0 경계). v1/v2 삭제. - 세트 데이터: 5단계만 → +5, 옛 난이도만 → 7·8·9·10 +
raw_payload.legacy_difficulty, 손댄 행은 임시 테이블에 기록해 자가 검증이 그 행만 본다. 그 뒤exercise_set_part의effort_level·effort_scale_version·difficulty삭제(CHECK 4개 자동 삭제). 변환 티켓set_config('lift_guild.repair_ticket', 'issue#1237')(#1236 절차) + 유실 감사 줄. - 관측 2테이블
perceived_rpe, 집계 3테이블rpe_set_count+ 6칸, CHECK 재생성. 함수 29개 재발행(통계 코어 3·하루 요약·달력 지표 2·빈 집계·무결성 3·세트 지표 맵·저장 답장 미리보기·저장 엔진 v5·WodUp/Motra 인입·읽기 투영 9 등). 저장 엔진은 옛 앱이 5단계 번호만 보내면 +5로 받는다(한 릴리스). - 완료 세션 사용자 전원 재계산 + 무결성 검사 3종 실제 호출(문제 0). 자가 검증: 변환 행·컬럼 부재·정책 활성·함수 본문 스캔(
difficulty단어 전수)·추정기 표본·최대 반복수 재계산/검사의 보조 무게 세트 제외 조건 일치. - 보호 층 재발행(#1236 절차): 등급 목록에서
effort_level·difficulty를 뺀 상태의 생성기 출력을 같은 이름의 새 마이그레이션에 담아 컬럼 삭제 직전에 적용 — 보호 트리거 함수가 원본 컬럼을 이름으로 비교하므로(new."difficulty") 이 순서가 아니면 컬럼 삭제 뒤 세트 UPDATE가 전부 실패한다. #1236 계약 테스트가 "같은 이름의 마지막 파일 = 생성기 출력"을 대조한다. - 최대 반복수 무결성 검사에 "보조 무게 세트 제외" 조건(재계산과 동일, #1202 누락 수리).
Phase 2 — 앱 자료형·어댑터
types/workout.ts세트 RPE 필드 6 → 1(perceivedRpe),barbelicCopy.ts의 5단계 상수 6개 →RPE_SCALE·RPE_SHORTCUTS·RPE_HALF_SHORTCUTS·normalizeRpe·formatRpe·rpeClassFor·restMinutesForRpe·rpeFromSetRecord. 저장 payload 키perceived_rpe만, 저장소 화이트리스트·검증(1.0~10.0 소수 1자리) 정리, "둘이 맞는지" 검사 삭제.- 읽기: 화면 RPC 어댑터·달력 읽기 모델·기간 통계 매퍼가
rpe_counts(6칸)·rpe_*_set_count를 검증·전달, 세트 행에서difficulty·effort_level키 삭제. 옛 초안·체크포인트·오프라인 대기열의 5단계 번호는 불러올 때 +5(한 릴리스). - 세트 목적 클라 정책(
setPurposePolicy.ts)을 RPE 7.0 경계로.
Phase 3 — 화면
- 세트 완료 카드 "SET N RPE": 숫자 입력칸(1.0~10.0, 0.5 스텝, 입력 버튼) + 단축 버튼 6 가뿐함·7 할만함·8 어려움·9 한계 근접·10 최대 노력 + 6.5~9.5. 세트 수정 모달 같은 구성("세트 RPE"). 세트 줄 칩 "@ RPE 8.5", 대기 표시 "RPE 입력 대기". 권장 휴식은 RPE 구간에서(8 미만 1분·8대 2분·9대 3분·10 4분, 미입력 2분). RPE 가이드에 "RPE 5 이하 — 웜업·가벼운 세트" 행.
- 데스크톱 표 열 "RPE", 세션 상세·일지 모달 RPE 칩 "RPE 8"(색은 RPE 구간), 도넛 "RPE 분포" 6칸(범례 6열 CSS), 모바일 리포트·하루 상세·세션 시트 4칸(6 미만·6·7–8·9–10) 통일. 완료 화면 칩도 RPE 숫자.
- 카드 DOM 훅
setRpeInput·composerRpeInput, 필드workout.set.perceivedRpe를 디자인 계약에 등재.
Phase 4 — 문서·기록
docs/data/rpe-performed-intensity.md(§1 표·§3 퇴역 규칙·§6 불변 조건), RPC 계약(+ko)·운동 화면 계약·세션 화면 계약(+ko)·로그 테이블·통계 dirty 이벤트(+ko)·계층 문서·PRD·종목 모델 정책 문구, 정책 README(e1rm v3·max-reps v2·set-purpose v3 라이브 사본), 컬럼 등급 목록.
주요 결정과 그 근거
- RPE 저장 범위 1.0~10.0 유지(오너) — 계획 v2의 0.0 확장은 폐기. 카드 숫자 입력칸도 1.0~10.0.
- 5단계 컬럼 물리 삭제 — "서버 내부 유지"는 두 벌 저장이 남아 같은 종류의 불일치가 재발한다(§22 근본 구조 개선).
- RPE < 6.0은 열린 구간 — RIR 하한만 안다는 뜻(웜업·가벼운 세트). 정책 3.0.0
open. - 한 릴리스 호환 — 옛 앱·옛 초안의 5단계 번호를 서버(
perceived_rpe_from_set_payload_v1)와 앱(rpeFromSetRecord)이 +5로 받되, 명시 RPE가 있으면 그대로 넘겨 범위 밖 값은 검증기가 거부한다(normalizeRpe로 삼키면 잘못된 입력이 조용히 사라진다 — 테스트가 잡았다).
작업 중 드러난 것
- 랜딩 순서 충돌 — #1215가 같은 함수 17개·앱 자료형을 바꾸며 랜딩 중이었다. #1215 브랜치 위에 쌓고, #1215·#1241 머지 뒤 main 위로 옮겨 함수 원문을 다시 뽑았다(달라진 것은 #1241이 고친 2개뿐 — 기계 대조로 확인). 이런 재발행 트랙은 "원문 덤프 → 정적 치환 → 조립" 파이프라인이 있어야 기저가 바뀌어도 하루 안에 따라간다.
- Motra 인입 함수의 빈
difficulty열 — 함수 목록 스캔이x.difficulty·'difficulty'꼴만 찍어 insert 열 목록의 맨 이름을 놓쳤다. 첫 pgTAP에서 잡혀 재발행에 넣고, 자가 검증 스캔을 "본문 어디든difficulty단어"로 넓혔다. - 시드 파서가 첫 insert만 읽음 — 정책 3.0.0 행이 베이스라인 뒤 마이그레이션에 있어 계약 테스트가 "seeded 아님"으로 봤다.
seededTable이 같은 표기(따옴표 식별자·::캐스트·한 줄 튜플)의 뒤 마이그레이션 insert도 합치도록 고쳤고, 마이그레이션의 행 표기를 그 꼴로 맞췄다. - 유실 감사·드라이런은 매일 데이터 사본으로 — 릴리스 v0.17.0(#1215 이름 변경)이 Production에 아직 없어 이 마이그레이션을 Production 되돌림 실행으로 검증할 수 없었고, 대기 12본을 한 트랜잭션에 묶은
db:loss-audit는 Management API 문장 시간 제한에 걸렸다. 대신User data daily copyartifact(2026-09-04 03:40Z, 세트 16,628행)를 로컬 샌드박스(Production 번호 꼬리20260909000100까지만 적용)에 복원하고 대기 12본 + #1236 4본 + 이 트랙 2본을 실제 순서로 적용해 감사했다: 파괴 0건·삭제 0행·빈 RPE 채움 227행·legacy_difficulty보존 119행·이력 227행(티켓issue#1237). 복원 기법: 마이그레이션은 파일마다psql -1(임시 테이블on commit drop이 있어 자동 커밋으로는 실패), 사본은session_replication_role = replica로 트리거·FK 끄고 넣되 카탈로그exercises는on conflict do nothing(id가 결정적 uuid라 겹침),auth.users는 profiles에서 만들어 FK를 채운다. - 실데이터 드라이런이 잡은 #1202 결함(사전 검증 효과) — 최대 반복수 재계산에는 "보조 무게 세트 제외"가 있고 무결성 검사에는 없어, 봉천곰돌이의 어시스트 딥 머신 3세트(Motra 인입)에서 검사가 실패해 마이그레이션이 되돌아갔다. #1202·#1241의 마이그레이션은 이 검사를 호출하지 않아 main에 잠복해 있었다. 검사 함수에 같은 조건 + 자가 검증 + pgTAP 3건. 근본 대책(자격 조건을 한 함수로 공유)은 남은 것.
- 오너 종목별 추정 1RM 전→후(사본 드라이런) — 159종목 중 144종목 불변, 15종목 변동(역대 최대값 변동 7: 덤벨 컬 40→41·바벨 점프 스쿼트 55→54·바벨로우 75→79·시티드 레그컬 38→40·파이크 푸쉬업 109→108·풀업 149→147·하이 스내치 82→84). 오르는 종목은 6~7월 옛 난이도 세트가 RPE를 얻은 것(D3-a), 내리는 종목은 RPE 6·7 세트의 RIR 표 변경(계획서 예고). 표 전문은 이슈 #1237 댓글(2026-09-04).
- main 합류(#1236 원본 보호) — 겹치는 함수 2개(
write_session_children_v5·build_session_detail_payload_v1)는 원문(mainc7b11d98)·#1236·이 트랙 세 판본을git merge-file로 병합(충돌 2곳 손 해소).git rebase는 CRLF 커밋 때문에 "patch does not apply"로 실패해merge로 대체했다(PR은 squash). - JS
replace의$$훼손(재발) — pgTAP 픽스처를String.replace로 넣다가$$가$로 바뀌어 46단언 중 41개만 도는 "Bad plan"이 났다(로컬 pgTAP에서 잡힘).split/join으로 교체. - 샌드박스 이미지 핀 — #1233 이후
ci:local이 핀(17.6.1.127)과 다른 이미지로 뜬 스택을 거부한다.--stop뒤 재기동. - 사전 검증 누락 없음: CI 실행 전
ci:local(pgTAP·e2e)·실데이터 사본 드라이런을 먼저 돌렸다.
5. 적용 결과
| 항목 | 전 | 후 | 확인 |
|---|---|---|---|
| 세트의 힘든 정도 저장 컬럼 | 3(effort_level·effort_scale_version·difficulty) + perceived_rpe | perceived_rpe 1 | 마이그레이션 ⑧·pgTAP hasnt_column |
| 관측·집계 테이블의 5단계 열 | 17 | 0 (RPE 열 23) | pgTAP·⑧ |
| 추정 정책 입력 | 5단계 표 | RPE 숫자(RIR = 10 − RPE) | pgTAP 추정기 표(RPE 6·7·8·9·9.5·10·5·미입력·실패) |
| 반값(8.5)이 추정에 반영되는 세트 | 0 | 전부 | pgTAP rpe8_half 11.5 |
| 입력 가능한 RPE 값 | 9개(6~10·반값) | 1.0~10.0 소수 1자리(91개) | 카드 숫자 입력칸·pgTAP 5.5 저장 |
| 5단계만 있고 RPE 없는 세트 | 108 | 0 | 마이그레이션 ⑧(변환 행 대조) — Production 프로브 2026-09-07: 5단계 컬럼 3개 부재, RPE 있는 세트 298(범위 6.0~10.0) |
| 옛 난이도가 RPE로 보이던 세트 | 119 | 0(RPE 7~10 변환·원값 보존) | Production 프로브: raw_payload.legacy_difficulty 119행 · user_fact_history 티켓 issue#1237 227행 · 활성 정책 e1rm 3.0.0 / max-reps 2.0.0 / set-purpose 3.0.0 |
| 클라 세트 자료 RPE 필드 / payload 키 / 짝 검사 | 6 / 5 / 1 | 1 / 1 / 0 | 타입·단위 테스트 |
| 데스크톱·모바일 도넛 칸 규칙 | 2가지(7월 이름표·4칸 불일치) | 1가지(6칸 / 4칸 내림) | 렌더 테스트 rpeMissingChart |
| 소스·문서의 "난이도" 표현(archive·versions·updates 제외) | src 27곳 + 문서 6개 | 0(퇴역 설명 문장 제외) | grep |
| pgTAP | 100파일/1686 | 104파일/1776(main #1236 합류 포함) | ci:local |
| 실데이터 유실 감사(사본 샌드박스) | — | 파괴 0·삭제 0·빈 RPE 채움 227·원값 보존 119·이력 227 | audit.sql(세션 scratchpad) |
| 오너 추정 1RM 변동 종목(사본 드라이런) | — | 15/159 (역대 최대값 변동 7) | 이슈 #1237 댓글 표 |
미검증: 카드·도넛의 시각 품질(사람 눈). Production 적용은 릴리스 v0.17.0(2026-09-06 머지, 레인 초록 — 마이그레이션 안의 무결성 검사 3종 통과 포함). 오너 종목별 추정 1RM 전→후 표를 Production 에서 다시 대조하지는 않았다(사본 드라이런과 같은 마이그레이션이 같은 데이터에 적용됐고 위 프로브가 변환 행 수와 일치).
6. 이번 개선으로 향상된 것
- 유저가 넣은 RPE 숫자 그대로가 저장·표시·계산의 값이다. 반값과 6 미만도 추정에 반영되고, 버튼에 없는 값도 넣을 수 있다.
- 서버·앱·문서·보고가 한 이름(RPE)을 쓴다. 데스크톱과 모바일이 같은 칸 규칙으로 같은 숫자를 그린다.
- 정책 3종의 입력 계약이 문서 지문·마이그레이션 표식·계약 테스트로 고정됐다.
- 함수 재발행 트랙의 작업 방식(원문 덤프 → 정적 치환 → 조립 → 자가 검증)이 남았다.
남은 것
- 다음 릴리스에서 한 릴리스 호환 제거: 서버
perceived_rpe_from_set_payload_v1의 +5 분기·저장 화이트리스트의 옛 키 3개(effort_level·effort_scale_version·difficulty,validate_completed_workout_payload_v4_structure_core·validate_session_payload_v5_core·completed_payload_v4_to_v5), 앱rpeFromSetRecord의 옛 초안 변환, 마이그레이션 ⑧의 "화이트리스트가 아직 옛 키를 받는다" 단언. - 최대 반복수 재계산·무결성 검사의 자격 조건을 한 함수로 공유 — 지금은 두 함수가 같은 조건을 따로 적고 자가 검증이 문자열로 대조한다(#1202 누락이 이 구조에서 났다). 세트 자격 판정 함수 1개를 두고 둘 다 호출하게 바꾸는 후속(§22).
- 세트 스코어 D7(
set_count복합 처리, #1189)은 이 트랙 범위 밖.