# 진행 상황 ## 종합 기획서·개발 현황 DocsSite 및 배포 연결 (2026-10-28) - `DocsSite/`: 종합 개요·모드 규칙·젤리 도감·장비/별 상점·전체 스테이지·개발 현황·AI 협업 안내. 현재 코드에서 69판·젤리 형상39종(실제배치35)·장비15개와 기존 이미지30장을 생성한다. HTML·문서별 Markdown·llms.txt·사이트맵·근거 코드 사본을 함께 제공한다. - 공개 주소 https://jelly-docs.steadyloop.dev, 문서 Worker `jelly-docs`. 기존 게임 Worker와 분리. 최초 수동 배포 및 HTTPS 기대 본문 검증 완료, 최신 수동 버전 `d112d462-dd0e-46be-ac52-7eeda01f7940`. - `.github/workflows/deploy-docs.yml`: main의 문서·게임 코드·원본 기획·입력 패키지/잠금·워크플로 변경 시 설치→테스트→검사→빌드→문서 배포. 문서 Secret `DOCS_CLOUDFLARE_API_TOKEN` 등록, 토큰은 해당 Worker의 Individual Workers Editor 및 steadyloop.dev의 Workers Routes Write만 사용한다. 기존 게임 토큰·배포 워크플로·게임 소스는 무변경. - 검증: 문서 테스트7/7·생성기 TypeScript·Astro 검사·빌드·정적 링크/이미지/앵커/AI 출력·실제 loopback 서버 시작/종료·별도 깨끗한 CI 설치/검사·Wrangler dry-run·실제HTTPS 통과. 코드/배포 최종 독립 리뷰 gpt-6.1-sol/medium의 필수 지적 수정 완료. - 같은 커밋의 기존 게임 Deploy도 성공: https://github.com/voidreader/CuttingJelly/actions/runs/40621528767, 단위144파일/2,055개 통과. 게임 소스는 이번 문서 작업에서 변경하지 않았다. - 사용자 승인: 기초 구현+사용자 시각 조정, Native, 실제 문서 배포·지속 토큰 범위/Secret 저장 및 main 커밋/푸시. 문서 Actions #1이 main 푸시로 실제 성공했고 공개 catalog의 커밋4c45f2c와 일치한다. 실행 https://github.com/voidreader/CuttingJelly/actions/runs/40621528754, 배포 버전9e997eb4-6762-4ad0-a746-cf0daf2ccb9c. 사용자 시각 검수·Windows 실실행·공식 런타임 실다운로드·네이티브 기기 검증은 미완료. - macOS `DocsSite/start-docs.command`, Windows `DocsSite/start-docs.cmd`. 실행·편집·배포·갱신 안내는 `DocsSite/README.md`. 상세 설계/검증 기록은 `docs/superpowers/{specs/2026-10-28-docs-site-design.md,plans/2026-10-28-docs-site.md}`. ## 별 상점·장비 교체 4단계: 압착 장비·장비 갤러리 — 기능 완료 (2026-10-28, `feature/equipment-shop`) - 계획: `docs/superpowers/plans/2026-10-28-equipment-shop-4-crush.md`. 로봇 주먹·마시멜로 망치(접촉 때 눌림)·고양이 발바닥(패드 퍼짐, 위로 옅어지는 다리) 압착 모델(`src/core/crush/`). `ModeHost.createPressModel()`이 `EquipmentModel` 전체를 넘기고 곰 벨트·찾기 장면이 `press(k)`→`update`→`dispose`를 맡는다. 압착 판정·움직임 상수는 그대로다. - **이제 장비 15개 모두 실제 모델이 있다(대체 모델 없음).** 1~3단계 항목의 "단계 4 병합 전 main 푸시 금지"는 해제됐다. main 푸시(운영 배포) 여부는 사용자가 결정한다. origin/main 뒤에 기능 밖 커밋 `473b068`(개발자 연속 진행·가이드 등장)도 아직 푸시되지 않았다. - 장비 갤러리: `docs/equipment-gallery/README.md`(장비마다 근접 이미지 + 실제 판 장면, 재생성 `npm run build && npx tsx scripts/equipment-gallery.mts`). - 검증: 단위 2,055개 통과, 빌드 통과, `npm run e2e:mobile` **356통과 / 21제외 / 0실패**(4183 설정). - 성능 A/B(모바일 뷰포트 390×844·dpr 3·CPU 4배 감속, 데스크톱 GPU): 1·14·59판에서 기본 장비와 단검·광선검·UFO·지팡이·피자커터·로봇 주먹·고양이 발 모두 프레임 p95 16.8ms 이하로 차이 없음. 장비 교체(모델 생성)는 45~86ms로 홈·상점에서만 일어난다. **실기기(Android·iOS) 측정은 하지 않았다.** - 시각 검수(에이전트, 전체 검수): 압착 3종 근접·곰 벨트 대기/접촉·찾기 압착, 갤러리 30장 확인. 캡처 `.playwright-mcp/equipment-4/`. - 최종 독립 리뷰(Opus / medium, `gpt-6.1-sol` 지정 불가): Critical 0, 출시 차단 코드 결함 없음. Important 1건(이 기록)은 반영. Minor: 운영 빌드에서도 `__jellyDebug.equip`·`previewEquipment`가 열려 있음(기존 디버그 계약 수준, 저장 불변), 컨텍스트 복구 직후 압착 모델 재예열 없음. - 남은 확인(사용자): 실기기 장비 외관·성능, 전체 시각 취향(특히 로봇 주먹 손가락, 고양이 다리 반투명, 광선검 대기 시 밝기 차이). ## 별 상점·장비 교체 3단계: 곡선 장비 (2026-10-28, `feature/equipment-shop`) - 계획: `docs/superpowers/plans/2026-10-28-equipment-shop-3-curve.md`. 피자커터(바퀴가 획 방향을 따라 구름, `followYawFree`)·만년필·초소형 UFO(세운 자세 `followUpright`, 아래로 쏘는 빔·빛나는 넓은 홈)·마법지팡이(그리는 동안 반짝이만, 손을 떼면 휘두르기 → 곡선 전체가 한 번에 빛남 → 기존 벌어짐, 추가 0.28초) 모델과 연출(`src/core/equipment/curve/`, `fx/`). `ToolEffects` 곡선 훅(`groove`·`strokeFollow`·`strokeEnd`·`prelude`·`fizzle`·`precompile`)과 `ScalpelGroove.setLook`, 공용 입자 풀 `ParticlePool`. 절단·평활화·제출·채점은 그대로다. - 검증: 단위 2,024개 통과, 빌드 통과, `npm run e2e:mobile` **353통과 / 21제외 / 0실패**(4183 설정). - 시각 검수(에이전트): 4개 장비 근접·14판 대기/그리는 중/절단/빗나감·지팡이 휘두르기·광선 단계별 캡처 `.playwright-mcp/equipment-3/`. - 최종 독립 리뷰(Opus / medium): Critical·Important 0. 대체 래퍼 `press` 전달·미리보기 움직임 줄이기·prelude 실패 테스트·모델 재질 예열은 4단계 Task 1에 넣었다. 모델 삼각형(곡선 30~42k)·물리 재질 비용은 4단계 성능 측정 대상. - (해제됨) 단계 4 병합 전 main 푸시 금지 — 4단계 완료로 해제. ## 별 상점·장비 교체 2단계: 직선 장비 (2026-10-28, `feature/equipment-shop`) - 계획: `docs/superpowers/plans/2026-10-28-equipment-shop-2-straight.md`. 중식도·당근칼·일본도·판타지 단검·광선검 모델(`src/core/equipment/straight/`, 공용 `BladeShaping.ts`)과 장비 연출 훅 `ToolEffects`(`src/cutting/ToolEffects.ts`, `KnifeAnimator`가 절단 단계에서 `cut`, `SlowKnifeRig`가 썰기 접촉 중 `slowContact`), 광선검 타는 연기·달아오름(`src/core/equipment/fx/`). 천천히 썰기 유령 칼날은 칼날의 모델 상대 변환·`ghostColor`를 따른다. - 검증: 단위 1,942개 통과, 빌드 통과, `npm run e2e:mobile` **344통과 / 21제외 / 0실패**(4183 설정). - 시각 검수(에이전트): 5개 칼 근접 미리보기·1판 대기/그리는 중/절단·광선검 연기 0.06~1.4초·회전판 29판·천천히 썰기 5판. 캡처 `.playwright-mcp/equipment-2/`. - 최종 독립 리뷰(Opus / medium, `gpt-6.1-sol` 지정 불가): Critical 0. Important 1건(연출 셰이더 예열)과 Minor(Home에서 연출 초기화·미리보기 연출 생성 생략)는 3단계 Task 1, 무거운 장비 성능 측정은 4단계에 넣었다. - (해제됨) 단계 4 병합 전 main 푸시 금지 — 4단계 완료로 해제. ## 별 상점·장비 교체 1단계: 기반 (2026-10-28, `feature/equipment-shop`) - (해제됨, 4단계 완료) 단계 4(압착 장비) 병합 전에는 main을 푸시하지 않는다. 상점에 새 장비 12개가 이미 보이지만 모델이 아직 없는 장비는 분류 기본 모델(식칼·메스·피스톤)로 임시 대체된다. 단계 2~4에서 모델을 채운다. - 설계: `docs/superpowers/specs/2026-10-27-equipment-shop-design.md`, 계획: `docs/superpowers/plans/2026-10-27-equipment-shop-1-foundation.md`. 사용자가 자리를 비우며 설계·계획·구현 승인을 위임했고 **Superpowers 전체 시각 검수**를 선택했다. 실행은 Subagent-driven. - 별 잔액 = 모은 별(스테이지별 최고 별 합) − 보유 장비 가격 합(0 미만 없음). 새 저장 키 `jelly-cutting:equipment`(`{version:1, owned, equipped}`), 기본 장비 3개는 항상 보유. 홈 왼쪽 위 별 숫자는 잔액이다. - 홈 별 표시 아래 `Shop` 버튼 → 상점 패널(All/Straight/Curve/Crush 탭, 장착·보유·구매 가능·잠김 상태, 구매 확인 → 바로 장착). 장비 15개 SVG 아이콘. - 장비 모델 교체: `EquipmentModel`·`MODEL_FACTORIES`(`src/core/equipment/EquipmentModels.ts`), `KnifeAnimator.setModel`, 천천히 썰기 유령 칼날 `syncKnifeGhost`, `ModeHost.createPressModel()`. 곰 벨트 압착기를 다른 젤리 찾기와 같은 원형 피스톤(`src/core/crush/PressModels.ts`)으로 바꿨다(판정·움직임 상수 불변). - 디버그: `__jellyDebug.equipment()`·`equip(id|null)`(세션 전용 덮어쓰기)·`previewEquipment(id|null)`(홈 근접 미리보기). - 검증: 단위 1,879개 통과, 빌드 통과, `npm run e2e:mobile` **335통과 / 21제외 / 0실패**(포트 4183 전용 설정 `.playwright-mcp/pw-4183.config.ts`, :4173은 메인 체크아웃의 옛 preview가 점유). - 시각 검수(에이전트): 390×844·1280×800·360×640·844×390 홈·상점·구매 확인, 곰 벨트 피스톤 대기·접촉, 찾기 피스톤. 캡처 `.playwright-mcp/equipment-1/`. - 리뷰: 작업별 리뷰 5회 모두 통과, 최종 독립 리뷰는 `gpt-6.1-sol`을 지정할 수 없어 **Opus / medium**으로 수행했다. Critical 0, Important 3건 중 PROGRESS 기록은 반영, 짧은 화면 로고는 360×640 캡처로 확인, 유령 칼날 변환·재질은 2단계 계획에 반영했다. ## 개발자 연속 진행·가이드 등장 연출·색 완화 (2026-10-27, `codex/dev-sequence-guide-intro`) - 설계·계획: `docs/superpowers/specs/2026-10-27-dev-sequence-guide-intro-design.md`, `docs/superpowers/plans/2026-10-27-dev-sequence-guide-intro.md`. 사용자가 설계·계획·구현 승인을 위임했고 **기초 구현 + 사용자 시각 조정**을 선택했다. - 개발자 목록·`gotoLevel` 이동은 세션 전용 `ProgressService.sequential`을 켜 결과 뒤 바로 다음 번호로 간다(이미 클리어한 판 포함, 마지막 판은 저장 진행 위치). 원인은 결과의 다음 판이 저장 진행 위치(클리어한 판 뒤 첫 미클리어)를 써서 50판으로 돌아간 것이다. 저장 진행 규칙은 그대로이며 Play·History·메인 복귀에서 끈다. 컨텍스트 복구 재로드 표시에 `sequential`을 더했다(옛 표시 호환). 전체 완료 뒤 개발자 이동 클리어는 종료 화면 대신 다음 번호로 간다. - 판 시작 가이드는 착지·메인 Play 뒤 0.3초 기다렸다 0.45초 동안 그리며 서서히 나타난다(`CuttingGuides.intro`, 움직임 줄이기면 즉시). 열린 가이드의 갈색 받침선을 밝은 색·알파 0.4·좁은 폭으로 완화했다. 틀 찍기 닫힌 윤곽 색은 유지한다. - 검증: 전체 단위 **133파일 / 1,847개** 통과(리뷰 반영 뒤 관련 2파일 19개 재통과), strict TypeScript·Vite 빌드·diff 검사 통과. 홈 E2E 모바일 **16개** 통과. `npm run e2e:mobile` **323통과 / 21제외 / 0실패**(리뷰 반영 전 빌드, 반영분은 테스트·문서만). - 독립 최종 리뷰는 도구 제약으로 `gpt-6.1-sol`을 지정할 수 없어 전역 지침의 **Opus / medium**으로 수행했다. Critical/Important 0건, Minor 테스트 공백 2건·캡처 안내 1건을 반영했다. 남은 관찰: 1·14판 튜토리얼 손가락이 가이드 등장(0.75초)보다 먼저 움직일 수 있음, 개발자 연속 진행 중 진행 위치 뒤의 미클리어를 깨면 기존 규칙대로 진행 위치가 옮겨감. - **시각 검수는 사용자 담당/미완료**: 390×844·1280×800에서 칼 모드 착지 직후~가이드 등장 완료, 밝은 과일 젤리 위 가독성. 조정 상수는 `src/cutting/CuttingGuide.ts`의 열린 가이드 `under`·받침 색, `GUIDE_INTRO_DELAY_SEC`·`GUIDE_INTRO_SEC`. Android 에뮬레이터·iOS 시뮬레이터는 사용하지 않았다. ## 홈 로고·버튼 교체 (2026-10-28, `codex/i-hate-jelly-home`) - 사용자 승인 C안 `I Hate Jelly` 투명 PNG를 홈 제목에 적용했다. 최종 색은 위쪽 부드러운 코랄, 아래쪽 옅은 살구색이며 CSS 로고 색상 필터는 없다. 홈 접근성 제목과 E2E 기대값도 변경했다. 인게임·앱 이름은 이번 범위 밖이다. - PLAY는 승인한 피치 젤리 이미지와 실제 HTML 글자를 조합했다. 최대 폭 250px, 기본 높이 94px(작은 세로 화면 64px, 낮은 가로 화면 55px), 채도 80%로 표시한다. 중간 시안 v1~v3도 사용자 요청에 따라 저장소에 보관하며, 실제 화면은 v4만 참조한다. - 사용자가 로컬에서 외관을 승인했다. 이미지 투명 경계의 미세한 색 잔점은 알려진 제한이다. 독립 코드 리뷰에서 수정 필요 결함은 없었다. - 검증: 빌드 및 전체 단위 133파일/1,844개 통과. 전체 E2E는 천천히 썰기 3건 실패 후 중단했다. 해당 3건 단독 재실행은 2통과/1실패: slow-food.spec.ts:130의 칼 화면 상단 여백(기대 >=2px, 실제 -0.468px). 홈 변경에 칼·카메라 로직 변경은 없다. 사용자가 해당 실패를 인지한 상태에서 추가 E2E 없이 main 머지·푸시를 명시적으로 요청했다. 빌드·전체 단위·홈 3브라우저 검사 통과 기록을 유지하며 운영에 반영한다. ## 다른 젤리 찾기 모드와 공통 곰 외형 개편 (2026-10-27, `codex/odd-jelly`) - 승인 설계·계획: `docs/superpowers/specs/2026-10-27-odd-jelly-design.md`, `docs/superpowers/plans/2026-10-27-odd-jelly.md`. 사용자는 **Native 구현 · Superpowers 전체 시각 검수**를 선택했다. - 최신 요청으로 찾기 모드의 다섯 색을 복원했다(색과 정답은 무관, 같은 색 정상 곰 존재). 눌린 개체만 양옆 14% 퍼짐·잔주름·우는 눈/입·파란 눈물을 표시하고 품질 교체 때 표정을 보존한다. 찾기 압착기를 원형 금속 접촉판·링·볼트·중앙 원통 피스톤으로 교체했다. 원본 폴리곤·게임 규칙·기존 벨트 장비는 유지한다. - 최신 다색·압착 조정 검증: 전체 단위 133파일/1844개·빌드·diff 검사 통과. 최초 전체 단위의 유일한 실패는 의도한 곰 셰이더 기준선 해시였으며 다른 종류는 동일함을 확인하고 곰 기준만 갱신했다. 찾기 세 브라우저 E2E 최초41통과/1기록실패는 병렬 Playwright CLI가 같은 test-results 폴더를 정리하면서 WebKit context.close의 trace ENOENT가 발생한 원인이다. 해당1개는 별도 결과 폴더로 단독 재실행해 통과했다. 기존 벨트 전원/보호색6개도 통과. 기본 두 해상도40개 상태 화면+얼굴8개+다섯 색 울음5개(총53개)를 확인했다. 독립 리뷰 gpt-6.1-sol / medium의 새 Critical/Important/Minor는 0건이다. 실제 신규 캡처는 low이며 high/medium 신규 화면·실기기·결과 상태 품질 교체 시각 검수는 추가하지 않았다. - 동일색이 흰색처럼 보이는 환경 반사를 찾기 전용 `softReflections` 옵션으로 낮췄다. 환경 강도·코팅·정반사를 줄이고 거칠기를 높여 붉은 몸통을 덮는 흰 반사를 완화했다. 기본 재질 값과 기존 압착기는 유지한다. 전체 단위 **133파일/1,841개**, 빌드·diff 검사와 관련 세 브라우저 E2E **9개** 통과. 모바일·데스크톱 low/high **18개 캡처**를 비교했으며 독립 리뷰(gpt-6.1-sol / medium)의 새 결함은 0건이다. medium은 재질 단위 검사만, 실제 화면은 low/high로 확인했다. - 웹 화면 피드백 후 격자를 없앤 고정 시드 분산 배치와 약 ±67.5도 회전을 적용하고 찾기 모드 전체를 빨강으로 통일했다. 기존 압착기 다색·보호색 규칙은 유지한다. 390×600·320×568에서 하단 안내와 겹침을 재현해 실제 안내 점유 높이를 카메라에 반영했다. 새 실제 외곽 비겹침 검사와 전체 단위 **133파일/1,839개**, 빌드가 통과했다. 기본 두 해상도 네 판과 대표 압착, 추가 작은 세로/가로 화면 **17개**를 검수했다. 가로 844×390에서는 고정 세로 배치를 축소하므로 미세 모양 식별이 어려운 제한이 있다. 관련 세 브라우저 E2E **42개 모두 통과**했고 diff 검사도 통과했다. 추가 독립 리뷰(gpt-6.1-sol / medium)의 새 결함은 0건이다. - 66~69판에 32개 곰 중 다른 젤리 3/5/7/9개를 30초 안에 찾는 모드를 추가했다. 난이도는 Easy/Normal/Hard/Very hard 순이며 변형 강도는 1/0.75/0.5/0.35다. 마지막 정답 탭 즉시 성공을 확정하고 마지막 압착 연출 뒤 결과를 표시한다. 오답마다 별을 하나 잃고 세 번째 오답 또는 시간 초과로 실패한다. Retry·Home·재로드에서 구성·위치·회전·색을 유지한다. - 개체 선택 강조·하늘에서 내려오는 압착판·눌린 뒤 색과 광택 변화, 남은 시간·발견 수·별·정상 견본을 연결했다. 변형 메시/SDF 준비는 프레임 분할하며 준비 취소·실패·이탈 자원을 해제한다. 숨김·복구·품질 교체·진행 저장과 디버그 계약을 검사했다. - 사용자 요청으로 **기존 압착기와 찾기 모드의 공통 곰**을 둥근 귀·통통한 몸·붙은 짧은 팔·발 형태로 함께 변경했다. 머리·몸·팔다리 타원의 부드러운 합집합에서 외곽을 만들고, 주둥이·배 양각은 곰 표면 법선에만 적용한다. 메시 품질·배치 비겹침·기존 압착판 정타 97% 이상을 확인했다. - 모바일 390×844·데스크톱 1280×800의 네 판 초기/선택/압착/눌림/오답 상태와 대표 결과, 320px 폭, 기존 59·61판 시작/접촉까지 기본 캡처 **63개와 얼굴 확대 4개**, 마지막 HUD 2개를 검수했다. 캡처는 `.playwright-mcp/odd-jelly/`(gitignore)에 있다. - 전체 단위 **133파일 / 1,838개 통과**, strict TypeScript·Vite 빌드 통과. 공통 곰 변경 전 전체 모바일 기능 검사는 **318통과 / 21제외**였다. 변경 후 관련 세 브라우저 E2E는 최초 **82통과 / 회전 2실패**였고 두 실패는 단독 재실행에서 통과했다. 추가 재현에서 CSS 크기만 바뀌고 렌더러·카메라가 갱신되기 전 테스트가 좌표를 읽는 원인을 확인해 렌더러 비율과 프레임 갱신 대기를 보완했다. 최종 빌드에서 찾기 모드 전체를 세 브라우저로 재실행해 **30개 모두 통과**했다. 마지막 관련 단위 확인 8파일/26개와 `git diff --check`도 통과했다. - 독립 최종 리뷰는 **gpt-6.1-sol / medium**으로 전체 브랜치와 공통 곰 추가 변경을 각각 확인했다. Critical/Important 0건. 후속 Minor 2건은 시간 초과 결과의 오답 수 문구 누락과 결과 상태 품질 교체 시 눌린 젤리 광택 복원이다(승패·진행에는 영향 없음). 상세 판단과 검증 경계는 설계 문서에 기록했다. - 네이티브 실기기 진동·GPU 외관과 전체 성능 회귀·장기 난이도 통계는 미검증이다. Android 에뮬레이터·iOS 시뮬레이터를 사용하지 않아 복제본 정리 대상은 없다. 작업 브랜치에 보존하며 main 병합·원격 푸시·운영 배포는 하지 않았다. ## 연속 벨트 곰 압착 개편 (2026-10-27, `codex/crush-bear-rework`) - 승인 설계·계획: `docs/superpowers/specs/2026-10-27-crush-bear-rework-design.md`, `docs/superpowers/plans/2026-10-27-crush-bear-rework.md`. 사용자는 **Superpowers 전체 시각 검수**를 선택했다. - 59~60판은 작은 다색 곰 8·10마리를 빠른 연속 벨트에서 모두 80% 이상 누르는 규칙으로 교체했다. 놓침·부분 압착 하나라도 실패다. 61~63판은 삭제했고 기존 `stage-064`~`068`의 고유 ID와 별 기록을 유지하며 표시 번호 61~65로 옮겼다. 삭제 ID에서 이어하던 사용자는 첫 유효 미완료 판으로 복원된다. - 곰 크기 0.85배, 압착판 반폭 0.90·깊이 반폭 0.70, 간격 0.42~0.49초로 조정했다. 타격 중에도 벨트가 흐르고 25ms 접촉 위치에서 판정한다. 오래된 정지형 모드·세션·디버그 키는 제거하고 공용 압착 장면·국소 눌림·진동은 유지했다. - 모바일 390×844·데스크톱 1280×800에서 59·60·61·65판 시작/이동/타격/성공·실패를 캡처해 안내 배지와 고정 HUD 간격을 조정·재검토했다. 캡처는 `.playwright-mcp/crush-bear-rework/`(gitignore)에 있다. 네이티브 실기기의 진동 체감은 이번 브라우저 검수에 포함되지 않았다. - 검증: 전체 단위 **124파일 / 1,808개 통과**, strict TypeScript·Vite 빌드·diff 검사 통과. 최종 모바일 Chromium 곰·디버그 E2E **17개**, 데스크톱 Chromium·WebKit 곰 E2E **10개**와 부분 압착 추가 **2개**, 진행·마지막 판 모바일 **3개**, 시각 캡처 **12개** 시나리오가 통과했다. 전체 성능 E2E는 실행하지 않았다. **gpt-6.1-sol / medium** 독립 최종 리뷰는 중요 결함을 발견하지 못했고, 제안된 품질 교체·숨김·복구/Home·저장 전환 E2E를 추가해 모바일에서 통과시켰다. Android 에뮬레이터·iOS 시뮬레이터는 사용하지 않았다. ## 곰 젤리 보호 압착 모드 (2026-10-26, `codex/bear-crush-sort` → main) - 승인 설계와 계획: `docs/superpowers/specs/2026-10-26-bear-crush-sort-design.md`, `docs/superpowers/plans/2026-10-26-bear-crush-sort.md`. 사용자는 **Superpowers 전체 시각 검수**를 선택했다. - 64~68판에 다섯 색 곰 젤리를 추가했다. 판마다 보호할 같은 색 곰이 2마리 이상이고, 나머지는 모두 80% 이상 눌러야 한다. 보호 곰을 조금이라도 누르거나 다른 곰을 놓치면 실수이며, 판은 끝까지 이어지고 실수 0개만 클리어다. 150ms 타격 주기 중에도 벨트가 계속 움직이고 실제 접촉 시점 위치로 판정한다. 첫 곰은 2.6초 뒤 중앙에 오므로 시작 안내를 읽을 시간이 있다. 기존 59~63판의 세션·속도·채점은 유지했다. - 시작 배지와 고정 HUD에 보호 색 이름·곰 아이콘·견본, `CRUSH THE REST`를 표시한다. HUD는 보호 성공 수·압착 성공 수·실수를 보여 준다. 곰은 개체별 색 유니폼을 쓰고, 실제 닿은 부분만 눌린다. 판정 진동은 기존 `FeelService`를 사용한다. - 모바일 390×844·데스크톱 1280×800에서 시작 안내, 다섯 색(흰색/노랑 포함), 접근·정타, 보호 곰 통과, 성공·실패 결과를 캡처해 크기·HUD 줄바꿈·배지 잘림·결과 안내 겹침을 조정하고 재검수했다. 캡처는 `.playwright-mcp/bear-crush/`(gitignore)에 있다. - 놓친 비보호 곰은 통과 순간 `Missed bear!`와 별도 진동을 한 번 내며, 보호 곰 접촉과 압착 부족도 구체적인 문구·진동으로 알린다. 최종 독립 리뷰(**gpt-6.1-sol / medium**)가 발견한 놓침 즉시 피드백 누락을 수정했고 재리뷰의 남은 Critical/Important는 0이다. - 최종 전체 단위 **125파일 / 1,815개 통과**, strict TypeScript·Vite 빌드·diff 검사 통과. 새 모드·기존 으깨기·디버그 관련 E2E는 모바일 Chromium·데스크톱 Chromium·WebKit **54개 통과**(마지막 HUD 문구·배지 조정 전). 마지막 피드백 수정 후 새 모드 E2E **6개 통과**(같은 세 브라우저). 사용자 요청대로 병합 시 E2E는 재실행하지 않았다. 기능 커밋 `a726ad6`을 main에 fast-forward 병합하고 origin/main에 푸시했다. CI·운영 배포 완료 상태와 네이티브 실기기 진동은 아직 확인하지 않았다. Android 에뮬레이터와 iOS 시뮬레이터를 사용하지 않아 복제본 정리 대상은 없다. ## main 푸시 전 검사 단축 (2026-10-26) - 사용자 요청에 따라 시각·입력 변경의 기본 푸시 전 E2E를 모바일 Chromium 기능 프로젝트로 한정했다. 명령은 `npm run e2e:mobile`이며 전체 E2E는 영향 범위나 명시적 요청에 따라 개발 중 선택한다. 근거는 `docs/superpowers/specs/2026-10-26-mobile-prepush-check-design.md`다. - 젤리 으깨기 모드 커밋 `874a71a`는 main에 병합·원격 푸시했다. 푸시 전 직렬 전체 E2E 967통과/45제외/0실패였다. 운영 배포 완료 상태는 별도로 확인하지 않았다. ## 젤리 으깨기 모드 (2026-10-26, `codex/jelly-crush`) - 승인 설계: `docs/superpowers/specs/2026-10-26-crush-mode-design.md`, 계획: `docs/superpowers/plans/2026-10-26-crush-mode.md`. 선택은 **기초 구현 + 사용자 시각 조정**, Native 구현 후 최종 독립 리뷰는 **gpt-6.1-sol / medium**. - `CrushMode`·`CrushSession`·`CrushScene`을 분리해 컨베이어 위 젤리를 캔버스 탭으로 압착한다. 겹친 면적만 채점하고 해당 부위만 낮춘다. 눌림은 이동 후에도 유지하며 파열·파편은 없다. 색상/깊이 셰이더가 같은 국소 변형을 사용하고 품질 교체 시 눌림 유니폼을 이어받는다. - 하강 45ms 뒤 접촉 때 진동 정타 [25]·부분 [15]·빈 타격 [8]을 기존 FeelService로 전달한다. 유지 35ms·복귀 140ms를 포함한 220ms 동안 벨트와 등장 시간은 정지하며 연타는 예약하지 않는다. 숨김·오버레이·준비 전 입력과 진행을 막고 한 젤리는 한 번만 판정한다. - stage-059~063의 5판(3~7개, 일정/불규칙 고정 간격)을 기존 58판 뒤에 추가했다. 기존 ID·콘텐츠·legacy 순서는 유지한다. 놓침을 포함한 평균 점수로 실패선 50·별 75/90을 적용한다. 기존 58판 완료 사용자는 59판을 이어한다. - 입력 시 메시 생성은 없고 enter에서 프레임 분할 준비·셰이더 컴파일 후 인계한다. 별도 다음 판 캐시는 생략해 진입 시 준비 대기가 있을 수 있다. 준비 취소/실패와 exit의 자원 해제를 검사했다. - 기능 E2E **132개 통과**(데스크톱 Chromium·모바일 Chromium·WebKit). 정타·앞/뒤 부분·빈 타격·연타·7개 완주·진동 끄기·숨김/재개·품질 교체·컨텍스트 복구·저장·Retry·Home·기존 모드·전체 완료·History를 확인했다. 최초 새 모드 E2E는 6통과/2실패였으며 테스트가 timeScale(0)을 복구하지 않고 전환을 기다린 원인을 수정한 뒤 모두 통과했다. - 추가로 디버그 계약 3개와 63판을 세 바퀴 도는 자원 검사 1개가 통과했다(기존 정책으로 메모리 모바일/WebKit 2개 제외). 로그: `/tmp/jelly-crush-e2e.log`, `/tmp/jelly-crush-memory.log`, 최초 실패 `/tmp/jelly-crush-e2e-first.log`. - 최종 단위 **122파일 / 1,798개 통과**, strict TypeScript·Vite 빌드·diff 검사 통과. 독립 리뷰(**gpt-6.1-sol / medium**)가 발견한 P1 모바일 카메라 far 초과 1건은 390×844·320×960에서 실패를 재현한 후 실제 카메라 거리+장면 크기에 맞게 far를 확장해 수정했다. 세 해상도 투영 검사와 기존 모드 far 복원 검사가 통과했다. 수정 후 새 모드·디버그 E2E **45개 모두 통과**(세 브라우저, 압착 중심의 GPU 픽셀 출력 포함). 다른 수정 필수 항목은 없었다. 로그: `/tmp/jelly-crush-unit-final.log`, `/tmp/jelly-crush-build-final.log`, `/tmp/jelly-crush-camera-red.log`, `/tmp/jelly-crush-camera-green.log`, `/tmp/jelly-crush-e2e-final.log`. - 시각 검수와 진동 체감은 **사용자 담당/미완료**. 390×844·1280×800에서 59판의 정타·부분 타격·빈 타격과 63판의 불규칙 리듬을 확인한다. 조정 상수는 `src/stages/Crush.ts`, 출렁임은 `CrushMode.updateScene`; 상세는 `docs/agent-reference/modes.md`. - 이 구현 기록 작성 당시 운영 배포·main 병합·푸시·앱 동기화·네이티브 기기 검증은 하지 않았다. 이후 main 병합·푸시는 위 항목에 기록했다. Android 에뮬레이터와 iOS 시뮬레이터/복제본을 생성하지 않아 정리 대상은 없다. ## 스테이지 고유 ID·순서 목록 분리 (2026-10-26, `codex/stage-order`) - 사용자 승인 설계대로 콘텐츠는 `StageDefinitions.ts`에 유지하고 `src/stages/stage-order.json`의 ID 배열만 옮겨 플레이 순서를 바꾼다. 불변 `stage-001`~`stage-058`과 현재 표시 번호를 분리했다. 최초 58개 순서·젤리·모드·가이드·채점 값은 유지한다. 기준 커밋과 콘텐츠 목록을 대조해 ID 변경 외 텍스트 동일을 확인했다. - `StageCatalog`가 모든 ID의 일대일 배치를 검증한다. 표시 번호·`getLevel(number)`·`?level=`·`__jellyDebug`는 현재 배치 번호 계약을 유지한다. 기존 콘텐츠 단위 검사는 고정 ID 카탈로그를 쓰므로 재배치와 독립이며, 실제 배치는 별도로 검증한다. - 진행·별·완료·방문은 `jelly-cutting:stage-progress` 단일 ID 객체로 저장하고 `SAVE_KEYS`에 등록했다. 구버전 키는 원본으로 보존하고 고정 `legacy-stage-order.json`으로 이관한다. 현재 순서로 옛 번호를 해석하지 않으며 반복 로드·쓰기 실패에도 +6을 중복 적용하지 않는다. 별 누락이 있는 전체 완료도 별 점수를 만들지 않고 완료 ID 목록으로 보존한다. - 재배치 후 하던 스테이지를 이어 하며, 일반 클리어는 다음 미클리어로 이동하고 끝에서는 앞쪽 미클리어로 돌아간다. History는 미클리어가 남아 있는 동안 이어하기를 유지한다. 전체 완료 뒤 새 콘텐츠가 추가되면 미클리어를 이어 한다. Game의 다음 판 준비·실제 이동·완료 화면이 같은 진행 값을 사용한다. - 설계: `docs/superpowers/specs/2026-10-26-stage-order-design.md`, 계획: `docs/superpowers/plans/2026-10-26-stage-order.md`. 관리 방법은 `docs/agent-reference/platform-levels.md`의 스테이지 순서 관리 절을 따른다. **순서를 바꿀 때 고유 ID와 legacy-stage-order.json은 수정하지 않는다.** - 최종 단위 테스트 **118파일 / 1,778개 통과**. 125개 합성 스테이지의 3→120 이동·30~35 구간 삽입, 기록 복원·앞쪽 추가·전체 완료·재플레이·쓰기 실패를 검사했다. 최초 관련 E2E는 **145 통과 / 2 제외**(데스크톱 Chromium·모바일 Chromium·WebKit)다. - 최종 독립 리뷰 **gpt-6.1-sol / medium**에서 History 재플레이로 마지막 미클리어를 완료할 때 전체 완료가 가려지는 Important 1건을 발견했다. 두 회귀 검사 RED→GREEN으로 수정하고 실제 History→전체 완료→재로드→마지막 판 Play E2E를 추가했다. 재리뷰의 남은 Critical/Important는 0이다. 수정 후 홈·진행 E2E도 세 브라우저에서 **93개 모두 통과**했다. 최종 strict TypeScript 검사·Vite 빌드·`git diff --check`도 통과했다. 로그는 `/tmp/jelly-stage-unit-final2.log`, `/tmp/jelly-stage-e2e.log`, `/tmp/jelly-stage-e2e-review-fix.log`, `/tmp/jelly-stage-build-final.log`에 있다. - 외관·배치·입력 조작은 변경하지 않았으며 별도 시각 캡처 검수는 범위 밖이다. main 병합·푸시·운영 배포·앱 동기화는 하지 않았다. Android 에뮬레이터·iOS 시뮬레이터 및 복제본을 생성하지 않았다. ## 실제 직선 절단·손을 떼지 않는 재베기 (2026-10-25, `codex/swipe-precise-recut`) - 휘둘러 베기 46~58은 중심·15° 보정 없이 실제 손가락 진입–이탈의 위치·각도로 직선 절단한다. 곡선은 제외한다. 56~58의 4조각 목표와 누름 단위 별 판정은 유지하며, 같은 누름으로 생긴 조각도 새로 통과하면 다시 벤다. - 설계: `docs/superpowers/specs/2026-10-25-swipe-precise-recut-design.md`, 계획: `docs/superpowers/plans/2026-10-25-swipe-precise-recut.md`. 사용자가 전체 시각 검수를 선택하고 설계·계획 승인 없이 판단대로 실행하도록 위임했다. - 12방향 결과 캐시·`KidPrep`을 제거했다. 원본 메시·외곽 SDF·셰이더만 발사 전에 준비하고 실제 절단 결과의 메시·SDF는 6ms 예산의 `PrepQueue`에서 생성한다. 준비 중 후속 입력을 자식에게 순서대로 재생하되, 적중 선분은 반복 적용하지 않는다. 조각 탄도는 적중 시각에서 이어진다. 유효하지 않은 스침·계산/자원 실패는 재시도 가능하며 입력 불가·Home에서 보관 입력·미완료 자원을 정리한다. - 50ms 속도 창의 순변위가 왕복 이동을 상쇄하는 문제를 실제 이동 거리 합으로 수정했다. 연속 베기 캡처에서 발견한 검은 섬광 잔상도 RGB 밝기 감쇠 대신 정점 RGBA·일반 알파 혼합으로 해결했다. 관련 실제 엔진·속도·알파 회귀 검사는 모두 RED→GREEN으로 확인했다. - `swipe().inputMeshBuilds`는 준비 큐 밖 입력 처리 중 동기 메시 생성 수(0)를 계측한다. `prepared`는 몸체 자원이 있고 결과 대기 중이 아닌 상태, `holdSwipeKidPrep`은 자식의 실제 절단 작업 보류다. 실제 결과 메시 생성은 큐에서 하므로 과거의 전체 베기 중 `meshBuilds` 증가 0 단언은 입력 처리의 동기 생성 0으로 바꿨다. - **최종 단위·빌드**: 116파일 / **1,761개 통과**, 타입 검사·빌드 통과. 최종 관련 E2E(Chromium·모바일 Chromium·WebKit)는 **150 통과 / 1 실패 / 20 제외**였다. 실패한 Chromium 53레벨은 첫 자동 마우스 호출 사이 168ms·676ms 지연이 생겨 속도 기준을 못 채운 입력이었다. 같은 검사를 단독 **5회 모두 통과**했다. 새 디버그 반환 키 고정 검사도 세 브라우저 모두 재통과했다. - **전체 회귀와 별도 모드 실패**: 섬광 수정 전 전체 E2E는 **889 통과 / 8 실패 / 45 제외 / 의존 성능 34 미실행**(26.9분). 실패 3건은 새 디버그 키 기대 누락(수정 후 재통과), 나머지는 천천히 썰기다. `slow-food.spec.ts`를 Chromium·WebKit에서 단독 재실행하면 **30 통과 / 2 실패**로 음식 젤리 9·10의 실패는 해소되고 마지막 칼 손잡이의 2px 화면 여백 검사가 두 브라우저에서 남았다. `slow-flow.spec.ts` WebKit 단독은 **7 통과 / 1 제외**로 레벨 10 쌓임 검사가 재통과했다. 변경 전 기준 `3d09a08`의 칼 손잡이 검사도 **WebKit에서 동일 실패**(1 통과 / 1 실패)해 기존 문제로 기록한다. 천천히 썰기 제품 코드는 변경하지 않았다. - **전체 시각 검수**: 390×844·1180×820·1280×800의 중앙·치우친 7°·비스듬한 37° 절단 9종과 실제 마우스/CDP 터치의 한 누름 4조각·섬광 소멸을 캡처·비교했다. 위치·각도 보존과 검은 잔상 제거를 확인했다. 최종 독립 리뷰 **gpt-6.1-sol / medium** 및 섬광 후속 리뷰 모두 Critical/Important 0. 문서·주석 Minor 3건은 갱신했다. 증거·로그는 `.playwright-mcp/swipe-precise-recut/`(gitignore)에 보관한다. - **성능**: 데스크톱 52·54·45→46 및 모바일 4검사 통과. 입력 처리 중 동기 메시 생성은 0이다. 58의 첫 데스크톱 준비 최대 9.9ms 초과를 기록하고 기준/변경 코드를 8회씩 교차 측정했다(아래 표). 준비 중앙값은 **6.7→7.9ms**, 획 처리 중앙값은 **1.25→2.15ms**다. 준비 8ms·획 16ms 예산 안이며, 기준 대비 차이는 판정선 max(1ms,20%)의 **1.34ms·1ms** 안이라 회귀 없음으로 판정했다. 개별 준비 8ms 단언 초과는 양쪽 **3/8회**로 남아 있으며 단언은 완화하지 않았다. | 검사 | 데스크톱 준비/획 처리(ms) | 모바일 준비/획 처리(ms) | |---|---:|---:| | 52 동시 베기 | 7.1 / 3.4 | 6.0 / 2.3 | | 54 동시 베기 | 5.9 / 3.4 | 7.2 / 2.4 | | 58 4조각 | 7.9 / 2.15 (8회 중앙값) | 5.9 / 3.3 | | 45→46 불러오기 | 1.8 | 1.4 | 58 교차 측정 준비 최대(ms), 기준 → 변경: `10.4→6.9`, `10.3→8.2`, `6.8→8.6`, `6.3→6.0`, `12.3→8.3`, `6.4→5.6`, `6.6→8.0`, `6.4→7.8`. - 운영 배포·main 병합·앱 동기화·네이티브 실기기 검증은 수행하지 않았다. Android 에뮬레이터·iOS 시뮬레이터 및 복제본을 생성하지 않았다. ## medium 추가 증폭 제거 (2026-10-25, 작업 트리) - 사용자 요청으로 일반 절단 medium의 버스 이득을 10(+20dB)에서 1로 복구했다. WAV는 변경하지 않았고 swipe·long의 이득 1, 공통 마스터 0.55·리미터·iOS status=0 로딩 수정은 유지한다. 이전 무음 원인은 로딩 실패였으므로 낮은 RMS 수치만으로 증폭을 강제한 테스트를 제거하고 추가 증폭 없는 재생 경로를 검사한다. - 적용 샘플 6개의 출처를 재확인했다. medium·long·swipe 2개는 현재 `references/higgsfield-audio/selected/` 파일과 SHA-256 일치. medium-01·02는 현재 해당 폴더에는 없고 이전 선택 처리 기록의 출력 SHA-256과 일치하는 복원본이다. 이번 요청에서는 파일 구성은 변경하지 않았다. - 전체 단위 **114파일 / 1,748개**, 타입 검사·빌드 통과. 최종 독립 리뷰 **gpt-6.1-sol / medium** 수정 필수 결함 없음. 실기기 청취와 새 앱 빌드·설치·업로드는 미완료다. ## 절단 후 가이드 잔상 수정 (2026-10-25) - 원인: 사용한 가이드를 불투명도 0.22로 유지해 벌어진 조각 사이 원래 위치에 남았고, 칼 모드는 마지막 절단 후 가이드 전체를 숨기지 않았다. 대비 강화로 연속된 어두운 받침선이 더 두드러졌다. - 사용한 직선·곡선·틀 자리 가이드는 0으로 페이드한다. 0.003 미만이면 불투명도를 정확히 0으로 고정하고 개별 메시를 숨긴다. 다음 자리를 강조해도 사용한 선은 되살리지 않는다. 칼 모드는 마지막 절단 연출 후 넘어짐·다시 자르기 전체 보기 복귀 전에 남은 선까지 그룹 전체를 숨긴다. - 두 회귀 검사의 수정 전 실패를 확인했다. 전체 단위 **114파일 / 1,749개**, 빌드·타입 검사·diff 검사 통과. gameplay·scalpel·recut·stamp·spin 브라우저 회귀 **216개**가 데스크톱 Chromium·모바일 Chromium·WebKit에서 통과했다. 완료 후 가이드 숨김·Retry 후 복원을 포함한다. 로그는 `/tmp/jelly-guide-unit.log`·`/tmp/jelly-guide-e2e.log`다. 최종 독립 코드 리뷰(**gpt-6.1-sol / medium**)에서 수정 필수 결함 없음. - main 푸시 전 전체 `npm run e2e`는 866 통과·10 실패·27 제외·34 미실행(30.4분)이었다. 데스크톱 Chromium의 천천히 썰기 저장·칼 여백·휘둘러 베기 56, WebKit의 천천히 썰기 화면 여백·재질 교체·투과 재질 검사가 실패했다. 저장값은 기대 6/실제 7, 화면 경계는 칼 y 약 -55px·조각 y 약 -13/-6px, 휘둘러 베기는 3/4 조각에서 fail이었다. WebKit 타임아웃 4건을 포함한 10건은 각 파일/최소 사례 단독 재실행에서 모두 통과했다. 모바일 Chromium과 WebKit의 동일 천천히 썰기 사례도 통과했다. 전체 실행 종료 무렵 load average는 1분 5.43·5분 63.24·15분 83.65였고, 단독 재실행 시 1분 6.14·5분 31.10·15분 64.14였다. - 성능 E2E 데스크톱은 15/17 통과했다. 레벨 45 틀 찍기 준비 시간은 기준 8ms 대비 8.5ms, 단독 재실행 10.4ms로 실패했고, 레벨 52 첫 측정 8.4ms는 단독 재실행 6.4ms로 통과했다. 모바일 성능은 17/17 통과했다. 변경 전 기준 커밋 `d6c4b33`의 레벨 45 동일 검사는 11.5ms로 실패해 기존 기준선에서도 재현됐다. 당시 이 성능 기준 실패로 main 푸시를 보류했다. 검증 기록 커밋 시점의 HEAD는 `c62fa37`, 원격 `origin/main`은 `d6c4b33`이다. - 이후 사용자 지시에 따라 레벨 45 `prepMaxMs` 검사는 `docs/superpowers/refactor-baseline.md` 6절의 기존 판정 규칙(수정 전후 각 3회 중앙값 비교, 허용 저하 max(1ms, 20%))에 맞췄다. 절대 8ms 단언만 제거하고 측정 로그와 결과 annotation 기록은 유지했으며, `strokeMaxMs ≤16ms` 단언과 다른 절단·메시 준비 기준은 유지했다. 이 변경 뒤 E2E는 다시 실행하지 않았다. `git diff --check`와 `tsc --noEmit`만 확인했다. main 푸시 후 CI 및 운영 배포 결과는 확인하지 않았다. - 사용자 선택은 **기초 구현 + 사용자 시각 조정**이다. 반복 캡처·이미지 비교는 수행하지 않는다. 로컬 웹에서 3(직선 두 칼질)·19(곡선)·30(회전판)·35(다시 자르기)·45(틀 찍기)를 플레이해 절단 후 빈 공간·결과 화면에 선이 남지 않는지, Retry 후 새 선이 정상 표시되는지 최종 시각 확인은 사용자 담당/미완료다. - 운영 배포·앱 동기화·네이티브 기기 검증·iOS 테스트/시뮬레이터 생성은 수행하지 않았다. ## 움직이는 목표를 따라 써는 리듬 시제품 (2026-10-25, codex/slow-rhythm) - 이전 속도 개선·고정 구간 비교를 체험한 사용자가 두 버전 모두 재미가 부족하다고 판단했다. 움직이는 마커를 리듬처럼 따라가고, 일치 진동과 놓친 뒤 복귀를 느끼는 체험용 시제품으로 전환했다. - 5~10레벨 모두 현재 Slice 하나의 큰 세로 게이지를 사용한다. 목표는 손을 댄 뒤 1.5초 기본 리듬(빠른 진입→부드러운 중간→빠른 마무리)으로 내려간다. 손을 떼면 정지, 뒤처지면 45% 속도로 도움, 앞서가면 목표를 기다려 다시 맞출 수 있다. 실제 칼은 더 빠른 따라잡기를 허용한다. - 리듬에 맞춰 움직이는 동안 80ms 간격 [8], 다시 맞추면 [16](최소300ms 간격), 완료 시 기존 판정 진동을 준다. 소리 고리는 실제 썰기에 맞춰 유지한다. 음식·칼질 수·저장·별 경계는 유지한다. 추가 체험에서 Perfect가 관대하다는 사용자 피드백을 받아 마커 중앙 일치 정도로 Rhythm 점수·판정·별·결과 막대를 연결했다. 중앙 ±0.025 만점, ±0.10에서 0점이며 실제 진행 깊이로만 적립한다. - 새 배지/튜토리얼/힌트와 결과 흐름을 연결했다. 이전 전체 E2E의 `debug-api` 기대 키 누락을 현재 `target` 계약 검사로 수정했다. - 독립 리뷰(gpt-6.1-sol / medium)에서 발견한 주사율별 펄스 누락을 실제 마지막 진동 시각 하나로 예약해 수정했다. 30~240Hz 회귀 검사와 재리뷰에서 해결 확인. 최신 점수 변경 재리뷰에서도 수정 필수 지적 없음. 전체 단위 114파일/1,747개, 최신 리듬·판정·진동 E2E 42개(세 브라우저) 통과. 이전 관련 207개 검사에서 연속 칼질 도우미의 준비 대기 누락을 수정했고 WebKit 여백 3건은 단독 재실행에서 모두 통과했다. 공유 preview 서버를 사용하는 중복 실행에서 발생한 연결 오류는 제외하고 직렬 실행으로 확인했다. 전체 E2E는 이번 리듬 변경 후 재실행하지 않았다. - 검수 방식은 기존 사용자 선택인 **기초 구현 + 사용자 시각 조정**이다. 게이지 위치·크기·가림·최종 외관과 실기기 촉감은 사용자 담당/미완료다. 운영 배포·앱 동기화·실기기 검증·iOS 시뮬레이터 생성은 수행하지 않았다. main 병합·푸시는 이번 요청에 따라 진행한다. ## iOS 번들 WAV 로딩 차단 수정 (2026-10-25, 작업 트리) - 최신 iOS에서도 버튼음만 들린다는 후속 보고를 조사했다. 설치된 Capacitor 8의 `WebViewAssetHandler.swift`는 WAV를 `HTTPURLResponse` 대신 `URLResponse`로 전달한다. 이때 fetch의 `status=0`·`ok=false`를 기존 로더가 실패로 처리해 일반 절단·swipe·long 샘플을 모두 버리는 경로를 확인했다. 합성 버튼음은 이 로더를 거치지 않는다. 앞선 음량 보정만으로는 이 로딩 차단을 해결할 수 없었다. - 두 WAV 로더에서 HTTP 상태가 없는 응답은 디코딩하도록 수정했다. HTTP 404·500은 거절하고 잘못된 오디오의 디코딩 실패 처리는 유지한다. 음량·원본 WAV는 바꾸지 않았다. - status=0 회귀 단위 검사는 수정 전 세 모드 모두 재생 소스 0개로 실패했고 수정 후 통과했다. 전체 단위 **114파일 / 1,737개**, 타입 검사·빌드, 실제 WAV를 사용하는 오디오 E2E **24개**(HTTP/status=0 × 식칼·메스·swipe·long × Chromium·모바일 Chromium·WebKit) 통과. 최종 독립 리뷰 **gpt-6.1-sol / medium**에서 수정 필수 결함 없음. - `cap copy ios`로 수정 웹 번들을 iOS 프로젝트에 복사했다. 새 IPA 빌드·설치·TestFlight 업로드와 실기기 청취는 미완료다. 시뮬레이터 목록 조회는 CoreSimulator 서비스 접근 실패로 완료하지 못했으며 시뮬레이터 실행·생성·복제본 생성은 하지 않았다. 운영 배포·푸시는 수행하지 않았다. 마지막 갱신: 2026-10-24 (칼 추적·가이드 대비·개발자 연속 진행 개선, 승인 soft-v2 효과음 게임 적용, R 구조 리팩토링 7·8단계(테스트·E2E 정리, 문서 마무리) 완료, 0~8단계 main 머지 `a65ecf4`, H 휘둘러 베기 46~58 main 머지(시각 검수는 사용자 담당·미완료), 홈 피치 UI·판정 타이포 B안 main 통합) ## 내리기 속도·큰 목표 게이지 비교 시제품 (2026-10-24, 작업 트리) - 사용자 요청으로 5~7레벨은 기존 HUD의 빠른 썰기, 8~10레벨은 빠른 썰기 + 현재 Slice 하나의 큰 세로 게이지·칼 마커·목표 구간으로 나눴다. 음식·칼질 횟수·저장·별 규칙은 유지한다. - 최대 진행·추종 속도를 두 배로 올리고 급함 판정의 허용 입력 속도도 함께 늘렸다. 일정하게 내리면 1.8초 안에 한 칼질을 99점 이상으로 완료하는 단위 검사를 통과한다. - 목표 구간의 부드러운 통과는 화면 반응과 1회 중간 강도 진동 [16]을 준다. 놓쳐도 진행·점수는 막지 않는다. 이동 중 [6]·완료 진동, 취소·숨김·진동 토글 경로를 유지한다. - 목표 판정은 실제 고정 적분 시간과 60ms 평활 속도를 사용한다. 독립 리뷰에서 찾은 주사율 편차를 수정했고 입력 필터를 연결한 30~240Hz 검사를 추가했다. 최종 독립 재리뷰(gpt-6.1-sol / medium)에서 남은 수정 필수 결함 없음. - 검증: 전체 단위 **114파일 / 1,734개 통과**, 빌드·타입 검사·diff 검사 통과. 내리기 관련 E2E는 데스크톱 Chromium·모바일 Chromium·WebKit 합계 **76개 통과 / 2개 제외**(데스크톱의 다중 터치 검사)다. 첫 회귀 실행에서 작은 끊김에도 기존 감점을 기대하던 판정 검사 9개가 실패했다. 새 허용 범위에 맞춰 거친 입력을 조정한 뒤 해당 파일 전체 21개가 재통과했다. 설계는 `docs/superpowers/specs/2026-10-24-slow-compare-design.md`. - 사용자 선택은 **기초 구현 + 사용자 시각 조정**이다. 모바일·태블릿에서 게이지 위치·크기·가림과 실기기 진동 손맛은 사용자 확인 대상/미완료다. 로컬 비교는 `?level=5`와 `?level=8`에서 시작한다. 운영 배포·앱 동기화·실기기 검증·iOS 테스트/시뮬레이터 생성은 수행하지 않았다. ## 칼 추적 안정화·가이드 대비·개발자 연속 진행 (2026-10-24) - 사용자 요청에 따라 Native 구현, Superpowers 전체 시각 검수를 수행했다. 브랜치 `codex/knife-guide-progression`. 스펙은 `docs/superpowers/specs/2026-10-24-knife-guide-progression-design.md`, 계획은 `docs/superpowers/plans/2026-10-24-knife-guide-progression.md`다. - 식칼·메스의 드래그 방향을 칼날 축으로 해석해 역방향에서 180도 뒤집지 않는다. 대기와 같은 기준 방향에서 ±25도로 제한하고 초당 90도까지만 회전한다. 실제 절단 정렬과 천천히 썰기는 기존 자세를 유지한다. 대기·드래그 불투명도는 0.5, 실제 칼질은 1이다. - 열린 직선·곡선 가이드에 짙은 갈색 연속 받침선을 더했다. 밝은 점선·흐름·끝 페이드·배정 흐림·새 가이드 공개를 유지하고, 닫힌 틀 가이드는 기존 동작이다. - 개발자 목록은 History 재플레이와 분리했다. Developer 30 클리어 → Retry 후 클리어 → 자동 31 진입, History 재플레이 중 디버그 강제 30 이동 → 수동 Next 31 진입, 완료 진도 59 보존, Developer 마지막 58 → 끝 화면을 검증했다. 이동만으로 저장 진도를 바꾸지 않으며 클리어 시 기존 최고 진도·별 저장 규칙을 따른다. - 검증: 빌드·타입 검사·diff 검사 통과, 전체 단위 **113파일 / 1,715개** 통과. 홈·진행·다시 자르기·틀 찍기 E2E **162개**, 최종 홈·인지(추가 진행 경계 포함) E2E **81개**가 데스크톱 Chromium·모바일 Chromium·WebKit에서 통과했다(두 실행은 일부 검사가 겹친다). 역방향·불투명도 및 개발자 Next 회귀의 수정 전 실패를 확인했다. - 시각 검수: 390×844·1280×800, 1·13·19·31 대기/정착 후 드래그/역방향, 13·14·19 대비/실제 절단(cut 또는 open)/완료 뒤 배정 가이드를 캡처했다. 밝은 용과·배와 토마토·키위의 대비를 확인했고 캡처 브라우저 오류는 없었다. 최종 독립 코드·시각 리뷰는 **gpt-6.1-sol / medium**, 초기 대기→드래그 회전 검사 누락을 보완한 뒤 수정 필수 코드 지적 없음. 증거와 로그는 `.playwright-mcp/knife-guide/`에 있다. - 기존 오디오 변경은 보존했다. 운영 배포·앱 동기화·네이티브 기기 검증은 수행하지 않았다. iOS 테스트를 실행하거나 시뮬레이터 복제본을 생성하지 않았다. ## iOS 일반 절단 저음량 보완 (2026-10-24) - 사용자 실기기 피드백은 iOS에서 클릭음은 들리고 일반 절단음은 들리지 않는다는 것이다. 기존 IPA 1.0.0(3)에 WAV 6개와 효과음 활성화 코드가 포함된 것을 확인했다. 실기기의 샘플 로딩 성공 여부는 아직 직접 확인하지 못했다. - 직전 적용에서 일반 절단 버스 이득을 10에서 1로 낮춰 재생 신호가 20dB 작아진 회귀를 확인했다. 실제 medium 3개의 최대 50ms RMS는 마스터 뒤 −36.5·−39.1·−41.5dBFS다. 일반 절단 이득 10을 복구했고, swipe는 별도 이득 1 버스로 분리해 원래 음량을 유지한다. long·마무리·마스터·리미터와 WAV 파일은 바꾸지 않았다. - 실제 PCM 샘플과 재생 이득을 사용한 음량 회귀 검사가 수정 전 −36.48dBFS로 실패하고 수정 후 통과했다. 리미터 전 신호 크기 검사이며 기기 출력·실제 청취의 대체가 아니다. - 검증: 전체 단위 113파일 / 1,709개, 빌드·타입 검사, iOS 번들 동기화 통과. 브라우저 오디오 검사에서 10개 통과·2개 실패(앱 복귀 후 비동기 resume 대기 누락) 후, 테스트에 running 상태 대기를 추가해 해당 경로 3브라우저 모두 재통과했다. 독립 리뷰(gpt-6.1-sol / medium)에서 제품 코드 수정 필수 지적 없음. iOS 설치·재빌드·스토어 업로드 및 실기기 청취는 미수행이다. CoreDeviceService 초기화 시간 초과로 기기 연결 상태를 조회하지 못했다. ## 승인 soft-v2 효과음 게임 적용 (2026-10-24) - 효과음 재생을 다시 켰다. 일반 절단(식칼·메스·틀 찍기)은 승인 medium 3개를 직전 것을 피해 랜덤 재생하고, 휘둘러 베기는 별도 swipe 2개를 랜덤 재생한다. 천천히 썰기는 long 1개를 실제 진행 중 반복하며 멈춤·손 뗌·통과·Home·숨김에서 페이드 정지한다. 이전 마무리 소리는 추가하지 않는다(`sliceEnd` 로그 유지). - 샘플 버스 이득은 모두 1로 두어 이전 ×10·×2.5·×3 재증폭을 제거했다. 마스터 0.55·끝단 리미터·Sound 토글·생명주기는 유지한다. 파일은 승인 soft-v2 그대로이며 medium-01·02는 남아 있는 승인 원본과 기록된 필터에서 결과 해시까지 동일하게 재현했다. - 근거: `docs/superpowers/specs/2026-10-24-selected-audio-design.md`, 파일 출처: `src/audio/samples/SOURCES.md`. - 검증: 전체 단위 113파일 / 1,708개 통과, 빌드·타입 검사 통과. 오디오 브라우저 검사 12개 통과(데스크톱 Chromium·모바일 Chromium·WebKit 각 4개). 독립 리뷰(gpt-6.1-sol / medium)에서 수정이 필요한 결함 없음. 실제 청취·실기기 출력 확인은 사용자 담당이며, 앱 동기화·배포는 하지 않았다. ## 피치 UI·판정 타이포 main 통합 (2026-10-23) - 사용자 요청으로 `codex/peach-home-ui`(`d61510b`)와 별도 업로드한 `codex/feedback-type-b`(`7551807`)를 main에 각각 머지했다. 충돌 없음. 로고·Play·판정·콤보 스타일을 함께 반영한다. - 통합본 빌드·타입 검사 통과, 홈·판정·콤보 E2E 15개 통과(세 브라우저 각 5개). 단위 1,466개 통과는 로고 브랜치 검증 결과이며 이후 변경은 CSS·문서뿐이다. 독립 코드 리뷰 지적 없음. - 기존 오디오 E2E 실패와 로고 가장자리 잔점은 아래 기록대로 남는다. 전체 E2E·시각 품질 완료로 간주하지 않는다. 사용자 요청에 따라 main을 푸시하며 운영 배포 결과는 해당 커밋의 Deploy Actions에서 확인한다. 네이티브 앱 동기화는 하지 않았다. ## 홈 피치 로고·Play (2026-10-23) ### 판정 타이포 B안 (2026-10-23, 위 main 통합에 포함) - 검증: 빌드·타입 검사·diff 검사 통과, 판정·콤보 E2E 12개 통과(데스크톱 Chromium·모바일 Chromium·WebKit 각 4개). 독립 코드 리뷰에서 조치할 결함 없음. 실플레이 배경별 가독성은 사용자 확인 대상으로 남긴다. - 사용자가 피드백 시안 B를 선택했다. `Perfect!` 허니 오렌지·`Great!` 코랄·`Good` 웜 토프의 Fraunces 600에 얕은 그라데이션과 부드러운 그림자를 적용했다. 기존 검은 외곽선은 제거했다. 콤보 `×N`도 같은 재질을 쓴다. 시안의 질감을 로컬 폰트·CSS로 구현하며 별도 래스터 글자 이미지는 추가하지 않았다. - 판정 문구·등급 기준·콤보·등장/유지/퇴장 시간과 위치 계산은 유지한다. 기초 구현 + 사용자 직접 외관 확인 방식이며 시안과의 최종 외관 일치는 사용자 담당이다. - 사용자 승인 방향: 기존 세리프 형태, 밝은 살구 Jelly·코랄 피치 Slice, 실제 투명 배경. 최종 자산은 `src/ui/jelly-slice-peach-v4.png`(RGBA 2148×732)이며 내장 이미지 생성 프롬프트를 함께 보존한다. 제목 접근성 텍스트·반응형 크기·현재 레벨의 실제 3D 젤리를 유지한다. - Play는 참고 이미지의 맑은 중심부·살구색 가장자리·두께 음영·윗면과 양끝의 반사광을 CSS로 표현한다. 브라운 Fraunces 글씨, 포커스·누름 효과·입력 경로를 유지한다. 게임 규칙·패널·HUD는 바꾸지 않았다. - 검수 방식은 사용자 선택인 기초 구현 + 직접 확인이다. 사용자에게 보여준 뒤 별도 브랜치 업로드 요청을 받았다. **로고 확대 시 색 경계 잔점은 여전히 남는다.** 이미지 생성에 따른 미세한 형태 차이도 시각 확인 대상으로 남기며 품질 해결 완료로 보지 않는다. Python 이미지 보정은 하지 않았다. - 검증: 빌드·타입 검사·diff 검사 통과. 전체 단위 80파일·1,466개 통과. 홈 E2E 36개 통과 후 최종 자산 변경에 대해 홈 진입·이미지 로딩을 재확인했다. 독립 코드 리뷰 지적을 반영했으며 현재 코드 결함 지적 없음. - 푸시 전 전체 E2E 764개 실행을 시도했으나 기존 `audio.spec.ts`의 식칼·메스 샘플 로딩 대기 2건이 시간 초과되어 중단했다. 기준 HEAD와 현재 코드 모두 `effects: false`이며 해당 테스트와 오디오 코드는 이번 작업에서 변경하지 않았다. 전체 E2E 통과를 주장하지 않는다. 로그는 `.playwright-mcp/peach-unit.log`, `.playwright-mcp/peach-full-e2e.log`에 있다. - 브랜치 `codex/peach-home-ui`. 이전 시안·`output/`·`public/assets/`는 로컬에 보존하고 최종 사용 자산만 커밋한다. main 머지·운영 배포·앱 동기화는 범위 밖이다. ## R 구조 정리 7·8단계와 R 마무리 (2026-10-24, main 머지 `a65ecf4`) - 스펙: `docs/superpowers/specs/2026-10-23-oop-refactor-design.md`(5절 테스트·E2E 정리, 7절 성공 기준), 계획: `docs/superpowers/plans/2026-10-24-oop-refactor-phase7-8.md`, 기준선·7·8단계 표: `docs/superpowers/refactor-baseline.md`(13절). 구조는 `AGENTS.md`의 '서비스와 모드 객체' 절(이력 서술을 걷어내고 최종 구조 기준으로 다시 씀)과 '명령'의 테스트·E2E 파일 목록. **제품 동작은 바꾸지 않았다**(제품 코드 변경은 export 하나 제거·주석·같은 계산의 경로 하나로 합침뿐, `__jellyDebug` 키·반환 모양 그대로). 시작은 main `079db48`(6·6.5단계 머지). - **한 일**(커밋 순): 1. `7fbcc73` 계획. 2. `28b4c9d` `tests/LevelRules.test.ts`를 모드·주제별 파일로 나눴다: `LevelOrder`(레벨 순서·`minAccuracy`·`intro`·젤리·사물)·`GuideLevels`(가이드 레벨)·`SlowLevels`·`SpinLevels`(D1 미룬 지적 4 포함)·`RecutLevels`·`StampLevels`·`SplitLevels`(반 가르기 레벨·사물 목록)·`StageSessionRules`(세션 규칙) `.test.ts` 8개와 공통 도우미 `tests/levelHelpers.ts`(`along`·`playPerfect`·`miss`). describe·테스트 이름·단언은 그대로다. 소스 주석의 `LevelRules` 참조 네 곳을 새 파일명으로 고쳤다. 3. `f27f807` E2E 공통 도우미를 `e2e/helpers/common.ts`로 옮겨 `e2e/helpers.ts`는 세 파일(`common`·`swipe`·`slow`)을 다시 내보내기만 한다. `helpers/*.ts`가 `./common`을 import하므로 `helpers.ts` ↔ `helpers/*.ts` 순환 import가 없어졌다. 본문이 글자 그대로 같은 지역 도우미(`isTouch`·`isTouchProject`·`step`·`timeScale`·`setHidden`·`waitDrawable`·`stubVibrate`, 휘둘러 베기의 `slowPath`·`waitAndFreeze`)를 합쳤다. 본문이 다른 것(`stamp.spec.ts`의 `step`·`pointer`, `swipe-flight.spec.ts`의 `pointer`, `slow-feel.spec.ts`의 `stubVibrate`, `home.spec.ts`의 `step`)은 그 spec에 남겼다. 모듈 수준 상태를 가진 도우미는 없어 공유 범위는 달라지지 않는다. 4. `bf48e4f` 디버그 조립 정리: `GameDebug.appDebug`의 불필요한 export를 지우고 `jellyScreenBounds`가 `screenBounds` 하나의 경로를 쓰게 했다(`jellyScreenBounds`·`jellyPixelsVisible`이 같은 함수를 공유, 값 동일). `JellyDebug.ts`·`Game`의 `gameTime` 대체값 `?? 0`·`StampMode.removeRemainder`의 이중 해제 방지(`remove(0, false)`) 주석을 고쳤다. E2E 헤더 주석 뒤 빈 줄을 정리했다. 5. 이 문서 커밋: `AGENTS.md`(서비스·모드 절 최종 구조 기준 재서술, 명령 절 파일 목록, 모드 공통 파일 목록), `PROGRESS.md`·`refactor-baseline.md` 13절·스펙 머리말. - **목록 비교 결과**(테스트 이름·개수가 옮기기 전후 같음): - 단위: `LevelRules.test.ts` 76개 → 새 8개 파일 76개, 파일 경로를 뺀 정렬 목록 diff 0. 퍼즈 파일을 뺀 `*.test.ts` 전체 901 = 901(diff 0, 오류 없음). 전체 `npm test`는 5단계 끝에서 106파일 / 1706개 → **113파일 / 1706개**(파일만 7개 늘고 개수는 같음). `npm run build` 통과. - E2E: `npx playwright test --list` 총 907 = 907(29파일, 정렬 목록 diff 0, stderr 없음). - **전체 E2E**: 전체 기능 E2E 결과는 `bf48e4f`(이후 커밋은 문서뿐)에서 **835 통과 / 11 실패 / 27 건너뜀**(26.3분)이다. 다른 앱이 기계를 크게 점유한 상태(load average 30~36)에서 쟀다. 실패 11개는 기존 `audio.spec.ts` 6개와 desktop-webkit 천천히 썰기 5개다: `slow-feel.spec.ts`의 `[Review Focus 1] 끄는 동안 진동 [6]이 반복되고 손을 떼면 멈춘다`(진동 기록), `slow-food.spec.ts`의 `[slow-food] 레벨 10: 6종 실제 음식의 결과·조각 수·20px 여백`(여백)과 `[slow-knife] 마지막 칼질 뒤 손잡이까지 움직임 전체에서 화면 안에 보인다`, `slow-judge.spec.ts`의 `세게 밀면 썬 조각이 뭉개진다`·`툭툭·세게 섞으면 ★ 또는 ★★`(시간 초과). 그 세 파일을 desktop-webkit에서 혼자 다시 돌리면 **28/28 통과**(1.4분)다. 판정: 부하 흔들림이고 기준선(840/6/27)과 같은 결과로 본다. 이 단계는 천천히 썰기 코드를 바꾸지 않았다(같은 본문의 e2e 도우미를 옮겼을 뿐). 표는 `refactor-baseline.md` 13절. - **성능 A/B는 하지 않았다**: 제품 코드 변경이 export·주석·같은 계산의 경로 정리뿐이라 프레임당 비용이 바뀌지 않는다(위험은 낮고, 틀렸다면 다음 성능 측정에서 드러난다). 출발점 E2E도 다시 재지 않았다(`079db48`은 6·6.5단계 마감에서 840/6/27을 잰 트리와 같다). - **스펙 7절 성공 기준 점검**: `Game`의 모드 판별은 모드 선택(레지스트리)과 예외 목록(시작 배지 `levelBadge` 호출·디버그 값 노출·장치 라우팅 `handlersFor`)뿐이다. 모드마다 클래스(와 협력 객체)가 입력·프레임·판 끝·HUD·결과·디버그를 갖고, 서비스와 `src/ui/` 공통 부품은 모드 이름으로 분기하지 않는다. `__jellyDebug` 키는 `e2e/debug-api.spec.ts`가 고정한다. 큰 파일은 책임 단위로 나뉘었고(테스트 `LevelRules`, E2E `slow`·`swipe`·`helpers`), `AGENTS.md`가 새 구조와 '모드를 고칠 때 볼 파일 / 새 모드를 넣을 때 손댈 곳'을 적는다. - **R 하위 프로젝트 전체 요약(0~8단계)**: `Game` 한 클래스가 다섯 모드를 `if` 분기로 맡던 구조를, 공통 서비스 네 개(`GameClock`·`FeelService`·`PieceStore`·`ProgressService`)와 `GameMode` 객체 네 종류(`StampMode`·`SwipeMode`·`SlowMode`·`KnifeMode`)와 `ModeRegistry`로 바꿨다. 모드를 단계적으로 옮겼고 단계마다 기능 E2E와 성능 A/B로 동작 보존을 확인했다. 단계별 main 머지: 0~2단계 `63f2178`(안전망 기준선 `9908c1a`, 서비스 4개, `GameMode`·레지스트리, 틀 찍기) → 3단계 `aaed554`(입력 처리기 일반화, 휘둘러 베기) → 4단계 `852b159`(인터페이스 정리, 천천히 썰기) → 5단계 `344ca11`(`PieceSplitter`, 칼 모드 `KnifeMode`와 `LegacyMode` 삭제, `SlowMode` 협력 객체) → 6·6.5단계 `079db48`(`handlersFor`, `GameDebug`, `LevelLabels`, HUD·결과 카드의 표시 데이터 `HudView`·`ResultView`) → 7·8단계(이 브랜치: 테스트·E2E 파일 정리, 문서 마무리). 결과: 단위 87파일 / 1534개(기준선) → 113파일 / 1706개, 기능 E2E 840 / 6 / 27(기존 `audio.spec.ts` 6개 실패)을 각 단계 마감 시점에 유지했고(중간 실행의 부하 흔들림은 혼자 다시 돌려 통과), 단계별 성능 A/B는 회귀 0이었다(7·8단계는 위 이유로 하지 않음). 규칙·수치·UI 문구·`__jellyDebug` 키는 바뀌지 않았다. - **R 이후 후보**(R 절들에서 미뤘고 아직 열린 사소한 지적. 모두 동작 결함이 아니라 정리·보강 후보다. 7·8단계가 해결한 것은 위 한 일 3·4번과 `LevelRules` 나누기다): - **코드·테스트 정리** - 움직임 줄이기 이중 검사(`FeelService.hitStop`과 `SwipeMode.onSwipeHit`의 `if (!reduced)`): 바깥 검사가 디버그 `swipe().hitStops` 카운트와 흔들림도 막아 지우면 값이 바뀐다. - `tests/SpinLevels.test.ts`의 회전판 가드 위치 검사가 `Game.ts` 소스를 `indexOf`로 읽는 문자열 단언이라 코드 모양이 바뀌면 깨지기 쉽다(7단계는 파일만 옮기고 단언은 그대로 둠). - `GameDebug`: `lastCut()`의 `unknown` 타입이 약하고, 디버그 키 순서를 고정하는 단위 테스트가 없다(키 이름은 `e2e/debug-api.spec.ts`가 지킴). `GameDebug.ts`가 three와 `SlowMode`의 `SLOW_HOVER` 상수를 값으로 import한다(core 계층이라 허용, 순환 없음). - HUD: `hud-swipes` testid에 "E2E가 읽는 계약" 주석이 없다. 셋째 칸이 null일 때 숨기기만 하고 라벨·값은 비우지 않는다. `SlowPanel`은 DOM이라 단위 테스트가 없다(E2E 몫). - 결과 카드: 휘둘러 베기 다음 별 문구는 DOM 지연 함수 안이라 단위 테스트가 존재 여부만 보고 `swipeNextText`·E2E가 전체 문구를 지킨다. 천천히 썰기 `slowResultView`는 브리프 시그니처에 선택 인자 `minAccuracy`가 더해졌다. - `Game`에 남은 칼 모드 보조(`SpinPlate`·`StrokeTrail`·`ShareLabels` UI): 조립만 `Game`에 있고 `KnifeMode`가 받아 쓴다. - `ProgressService`(던지는 저장소 테스트·`levelCount`와 `LEVELS` 혼용·`progress.progress` 이름)와 `Game`의 `haptics`·`audio` 직접 사용. - `e2e/swipe-flight.spec.ts`·`swipe-life.spec.ts`·`swipe-pieces.spec.ts`에 같은 오류 수집 `beforeEach`/`afterEach`가 아직 복제돼 있다(`helpers/common.ts`로 모을 후보). - `tests/levelHelpers.ts`의 `miss`는 `StageSessionRules.test.ts` 한 파일만 써서 그 파일로 옮겨도 된다. `LevelDefinitions.ts`의 주석이 이미 없는 `LevelRules` 테스트("집어 들자마자 떼기")를 가리킨다. - **테스트 보강** - `PieceSplitter`(`swap` 직후 `squash`·`impulse` 확인), `SplitShares`(가짜 `ShareLabels` 단위만, 실제 DOM 라벨은 E2E), 5단계 Task 4 리뷰 minor(`sweepPieces` 단언이 `not.toBe`, 장치 취소 → 처리기 취소 → `exit` → `enter` 순서의 단위 검사 없음, 레지스트리 fallback 단언이 따로 만든 `KnifeMode` 인스턴스를 봄), `SlowBarsView`의 `cuts` 캐시와 `SlowParts` 테스트가 `pose`·`tip`을 직접 검증하지 않는 것. - 식칼 연출이 `KnifeCutFx.present`(async)를 거쳐 마이크로태스크가 한 단계 늘었다(프레임·트윈 순서 영향 없음, 허용한 차이). - **측정·실기기** - `SlowCut` 서브스텝 몰림 민감성: 프레임이 끊기는 기기에서 부드러운 끌기가 `jerky`로 읽힐 수 있다(실기기 미확인, 체크리스트에서 볼 것). - WebKit `slow-food.spec.ts` 레벨 10 화면 여백(17.39 < 20)의 부하 흔들림(다시 나오면 부하 조건을 먼저 본다)과 mobile-chromium `home.spec.ts` 천천히 썰기 레벨 History 재플레이 테스트의 한 번 시간 초과(혼자 12/12, `--repeat-each=10` 10/10, 테스트 타이밍 결함 후보). - 모바일 perf 45 `prepMaxMs`·25/28 `strokeMaxMs`의 예산 경계 흔들림(양쪽에 나오는 기존 현상). 관찰: desktop `21→22 dropMaxGapMs`의 높은 갈래(27~39ms) 빈도(6·6.5단계 A/B에서 영향 없음, 5단계 이전 대비는 모름, 실기기에서 다시 볼 것)와 mobile `level52 swipe prepMaxMs`의 1회성 스파이크. ## R 구조 정리 6·6.5단계: `Game` 마무리·UI 모드별 표시 (2026-10-24, 브랜치 `refactor/oop-phase6`, main 머지 `079db48`) - 스펙: `docs/superpowers/specs/2026-10-23-oop-refactor-design.md`, 계획: `docs/superpowers/plans/2026-10-24-oop-refactor-phase6.md`, 기준선·6·6.5단계 표: `docs/superpowers/refactor-baseline.md`(12절). 구조는 `AGENTS.md`의 '서비스와 모드 객체'(`handlersFor`·`hudInfo`·`resultView`·`ModeHost.ui.hidePanel`·`GameDebug`)·'UI 표시 부품'·'`loadLevel`의 모드 호출 순서'·'입력 연결'·'`Game`에 남은 모드 분기'·'디버그 진입점' 절. **동작은 바꾸지 않았다**(레벨 1~58의 규칙·수치·UI 문구, `__jellyDebug` 키·반환 모양·DOM 구조). 시작은 main `344ca11`(5단계 머지). - **한 일**(커밋 순): 1. `82b25c9` 계획. 2. `f983f1b` 입력 처리기 null 안전 호출과 정리: `src/core/modes/InputRouting.ts`의 `handlersFor(mode, kind)`(종류가 같고 처리기가 있을 때만 처리기, 아니면 null)를 세 장치 콜백이 거치고 `Game`의 비null 단언(`stroke!()`·`depth!()`·`swipe!()`)이 없어졌다. 회전판 가드(`plateOutline`)를 `loadLevel` 첫머리(어떤 상태도 바꾸기 전)로 앞당겨 5단계에서 늦춰진 던지는 시점을 복원했다(`KnifeMode.plateOutline`은 `host.shape()` 대신 `getJellyShape(level.jellyType).outline`을 직접 씀). `Game.cancelDevices`가 이미 취소하므로 `SwipeMode.exit`·`cancelInput`의 `recorder.cancel()` 중복을 지웠다. `SlowMode`의 테스트 호환용 private 접근자(`slowMoving`·`slowDebrisPoint`·`slowPlane`)를 지우고 테스트를 `rig`로 옮겼다. 3. `4da6d54` `__jellyDebug` 조립을 `Game.installDebug`에서 `src/core/GameDebug.ts`의 `gameDebug(ctx)`로 옮겼다(`GameDebugContext`는 `Game`이 `debugContext()`로 만듦, 모드별 `debug()` 캐시와 E2E 설정 키 전달도 이동). 모드가 내지 않는 키의 기본값은 `MODE_DEBUG_DEFAULTS`(호출마다 새 값을 내는 팩토리 함수 10개)다. 63개 키의 순서·값은 옛 코드와 같음을 대조했다. 4. `c4d0a8f` 레벨 방식 문구를 `src/stages/LevelLabels.ts`(순수 `levelLabels(level) → { badge, menu }`)로 모았다. `levelBadge`와 개발자 목록 라벨이 같이 쓰고, `MainMenu`는 `levelLabel` 옵션만 받아 `src/ui/*.ts`에 `level.mode`·`.spin`·`.recut` 분기가 없다. 5. `2007a44` HUD를 `HudView`(`levelId`·`target`·`countLabel`·`used`·`max`·`extra`)만 그리게 하고 `hudInfo()`를 모드 필수 메서드로 만들었다(`HudState`·`unit` 삭제). 천천히 썰기 패널(상태·깊이 막대·준비 실패 안내)은 `src/ui/SlowPanel.ts`, 공용 `KNIFE_ICON`은 `icons.ts`, 목표 문구·기본 칼 레벨 표시는 `hudText.ts`다. `ModeHost.ui.setSlow(null)` 대신 `hidePanel()`(휘둘러 베기도 준비 실패 안내를 지우는 데 씀)이다. 6. `654c364`·`0dd8343` 결과 카드는 `ResultView`만 그리고 모드별 문구·extras는 `src/ui/ResultViews.ts`(`knifeResultView`·`stampResultView`·`slowResultView`·`swipeResultView`)에서 만든다. `resultView`가 모드 필수 메서드가 되고 `resultExtras`와 `ResultInfo`의 모드 키(`stamp`·`slow`·`swipe`)가 없어졌다. `0dd8343`은 리뷰 지적으로 Slow·Swipe 모드 테스트의 빌더 인자(데이터) 단언과 문구 라벨 단언을 되살렸다(테스트만). - **기준선 대조 결과**(기준선 `9908c1a`, 단계 시작 `344ca11`. 표는 `refactor-baseline.md` 12절): - **출발점**: `344ca11`의 트리가 5단계 마감에서 측정한 트리(`8abfeb7` 뒤 머지)와 같아 다시 재지 않았다(5단계 마감 값: 기능 E2E 840/6/27, 성능 A/B 회귀 0). - **단위 테스트**: 5단계 끝 101파일·1680개 → **106파일·1706개**(새 테스트 `InputRouting`·`GameDebug`·`LevelLabels`·`HudText`·`ResultViews`와 `SwipeGameMode`·`SlowGameMode`·`KnifeMode`·`StampMode`·`ModeRegistry` 보강). 인터페이스가 바뀐 단언은 같은 화면 결과를 뜻하는 단언(`unit: 'slices'` → `countLabel: 'Slices'`·`target: '6 pieces'` 등)으로 옮겼고 단언을 지운 테스트는 없다. `npm run build` 통과. - **UI E2E(Task 5, 12개 파일 × 세 프로젝트, 한 번)**: 382 통과 / 0 실패 / 5 건너뜀. - **기능 E2E 전체(세 프로젝트, 코드 `654c364`, 이후 커밋은 테스트·문서만, 20.4분)**: **840 통과 / 6 실패 / 27 건너뜀**으로 기준선(840/6/27)과 같았다. 실패 6개는 모두 기존 `audio.spec.ts`다. - **성능 E2E 단계 A/B(`344ca11` → HEAD, 같은 시각 번갈아 3회, 후보 11회)**: **회귀 0**. 1차 후보는 mobile `level52 swipe prepMaxMs`(6.00 → 7.40) 하나였고 11회에서 6.00 → 6.30으로 판정선 안이었다(head에서 9.0·12.0이 1회씩 튐, base는 8.2 1회. 휘둘러 베기 준비 코드는 이번 단계에서 바꾸지 않아 1회성 스파이크로 보고 지켜본다). 단언 넘김은 양쪽에 나온 알려진 예산 경계(45 stamp `prepMaxMs`, 58 swipe pieces `prepMaxMs`)뿐이다. 표는 `refactor-baseline.md` 12.3절. - **`Game`에 남은 모드 분기(명시적 예외 목록)**: 시작 배지 `levelBadge` 호출(문구는 `LevelLabels` 순수 표), 디버그 `mode`·`tool` 값 노출(이제 `GameDebug` 안, 분기 아님), 장치 라우팅(`handlersFor` 한 곳). 5단계 목록의 "디버그 노출 방식"·"비null 단언"이 이것으로 정리됐다. 새 모드가 이 목록을 더 늘리지 않게 한다. - **미룬 사소한 지적(작업별 리뷰 통과, 고치지 않고 다음 계획으로)**: - **6·6.5단계에서 나온 것**: - 움직임 줄이기 이중 검사(`FeelService.hitStop`과 `SwipeMode.onSwipeHit`의 `if (!reduced)`)를 지우지 않았다. 바깥 검사는 디버그 `swipe().hitStops` 카운트와 흔들림도 막아 지우면 값이 바뀐다(동작 보존 위반). - `LevelRules.test.ts`의 회전판 가드 위치 검사가 `Game.ts` 소스를 `indexOf`로 읽는 문자열 단언이라 코드 모양이 바뀌면 깨지기 쉽다(7단계에서 `LevelRules` 나누기와 함께 볼 것). `Game`의 `gameTime` 대체값 `?? 0`에 주석이 없다. - `GameDebug`: `appDebug`는 같은 파일에서만 써서 export가 불필요하다. `GameDebugContext`에 `jellyScreenBounds`와 `screenBounds` 두 경로가 따로 있다(7단계 후보). `lastCut()`의 `unknown` 타입이 약하고, 디버그 키 순서를 고정하는 테스트가 없다(키 이름은 `e2e/debug-api.spec.ts`가 지킴). `GameDebug.ts`가 three와 `SlowMode`(상수 `SLOW_HOVER`)를 값으로 import한다(core 계층이라 허용, `SlowMode`가 `GameDebug`를 import하지 않아 순환 없음). - HUD: `hud-swipes` testid에 "E2E가 읽는 계약" 주석이 없다. 셋째 칸이 null일 때 숨기기만 하고 라벨·값은 비우지 않는다(숨김이라 영향 없음, 초기 라벨은 이제 빈 글자이고 보일 때는 `extra.label`(`Swipes`)로 같다). `SlowPanel`은 DOM이라 단위 테스트가 없다(E2E 몫). `hudTarget`·`defaultHudView`는 `HUD.ts`가 아니라 `hudText.ts`에 있다. - 결과 카드: 휘둘러 베기 다음 별 문구는 DOM 지연 함수 안이라 단위 테스트가 존재 여부만 보고 `swipeNextText`·E2E(`결과 카드: Swipes 값·다음 별 안내`)가 전체 문구를 지킨다. 천천히 썰기 `slowResultView`는 브리프 시그니처에 선택 인자 `minAccuracy`가 더해졌다. - 5단계 목록의 `Game`에 남은 칼 모드 보조(`SpinPlate`·`StrokeTrail`·`ShareLabels` UI)는 이번 단계에서 손대지 않았다(조립만 `Game`에 있고 `KnifeMode`가 받아 쓴다). - **5단계에서 넘어온 것 중 아직 열린 것**: `PieceSplitter` 테스트 보강 후보(`swap` 직후 `squash`·`impulse` 확인), `SplitShares`의 가짜 `ShareLabels` 단위 테스트(실제 DOM 라벨은 E2E), Task 4 리뷰 minor(`sweepPieces` 단언이 `not.toBe`, RF5 장치 취소 → 처리기 취소 → `exit` → `enter` 순서의 단위 검사 없음, 레지스트리 fallback 단언이 따로 만든 `KnifeMode` 인스턴스를 봄), 식칼 연출이 `KnifeCutFx.present`(async)를 거쳐 마이크로태스크가 한 단계 늘어난 것, `SlowBarsView`의 `cuts` 캐시와 `SlowParts` 테스트가 `pose`·`tip`을 직접 검증하지 않는 것, 출발점 WebKit `slow-food.spec.ts` 레벨 10 화면 여백(17.39 < 20) 부하 흔들림(다시 나오면 부하 조건을 먼저 본다). - **4단계 이전부터 아직 열린 것**: `SlowCut` 서브스텝 몰림 민감성(실기기 미확인), 디버그 `slowVisual()`의 `plane`·`generation`이 slow가 아닌 레벨에서 고정 진단값인 것(의도, 닫음), mobile-chromium `home.spec.ts` 천천히 썰기 레벨 History 재플레이 테스트의 한 번 시간 초과(테스트 타이밍 결함 후보), 3단계에서 넘어온 `removeRemainder`의 `pieces.remove(0, false)` 주석(7·8단계에서 해결)·`ProgressService`(던지는 저장소 테스트·`levelCount`와 `LEVELS` 혼용·`progress.progress` 이름)·`Game`의 `haptics`·`audio` 직접 사용·`e2e/helpers.ts` ↔ `e2e/helpers/*.ts` 순환 import·지역 도우미 복제(7·8단계에서 해결: 본문이 같은 것만 합침), 모바일 perf 45 `prepMaxMs`·25/28 `strokeMaxMs`의 예산 경계 흔들림. - 5단계에서 넘어와 이번 단계에서 해결: 장치 취소 중복(`SwipeMode`)·비null 단언·`SlowMode` 테스트 호환 접근자·회전판 가드 위치(최종 리뷰 F2·F3)·`ModeHost.ui.setSlow` 이름과 `finishLevel`의 `stamp: false, slow: undefined, swipe: undefined` 기본 키·`levelBadge`/디버그 노출 방식. - **관찰 항목 결과**(5단계에서 넘어온 desktop `21→22 dropMaxGapMs` 높은 갈래(27~39ms) 빈도): 11회 중앙 19.20(`344ca11`) → 18.90(HEAD), 높은 갈래(26ms 이상) `344ca11` 4/11·HEAD 3/11로 같아 6·6.5단계는 영향이 없다. 이 base가 이미 5단계 뒤라 5단계 이전 대비 빈도 변화에는 답하지 않는다(필요하면 실기기·7단계에서 다시 볼 것). - **다음 계획(당시): 7 테스트·E2E 정리, 8 문서 — 완료(위 7·8단계 절).** 7단계: `tests/LevelRules.test.ts` 모드별 나누기, 큰 테스트·E2E 파일 정리, `e2e/helpers.ts` 순환 import 정리와 위 미룬 테스트 지적. 8단계: 문서(스펙 7절 최종 점검). 계획 문서는 단계마다 새로 쓴다(`superpowers:writing-plans`). 이 단계는 최종 독립 리뷰(Opus / medium) 뒤 사용자 지시로 main에 머지했다(`079db48`). ## R 구조 정리 5단계: 칼 모드 이전 (2026-10-24, 브랜치 `refactor/oop-phase5`, main 머지 `344ca11`) - 스펙: `docs/superpowers/specs/2026-10-23-oop-refactor-design.md`, 계획: `docs/superpowers/plans/2026-10-24-oop-refactor-phase5.md`, 기준선·5단계 A/B 표: `docs/superpowers/refactor-baseline.md`(11절). 구조는 `AGENTS.md`의 '서비스와 모드 객체'(`KnifeMode`·`PieceSplitter`·새 훅)·'칼 연출과 상태 기계'·'회전판(D1)'·'조각 다시 자르기(D2)'·'천천히 썰기 (G)' 절. **동작은 바꾸지 않았다**(레벨 1~58의 규칙·수치·UI 문구, `__jellyDebug` 키·반환 모양). 시작은 main `852b159`. - **한 일**(커밋 순): 1. `49f468c`·`1066aef` 계획. 2. `480d6d0` 공용 조각 교체 `src/core/PieceSplitter.ts`(옛 `Game.prebuildParts`·`swapSplits` 본문 그대로). `Game`은 `splitter`를 갖고 `ModeHost.prebuildParts`·`swapSplits`는 위임한다. (4단계에서 미룬 `prepMaxMs` 갱신 중복도 `Game.addPrepMs` 한 곳으로 모았다. 5단계 안에서 `5591ebe`가 했다.) 3. `05ccb77` 칼 모델·가이드 원천·튜토리얼 대상을 모드 훅으로: `GameMode.tool(level)`(필수, `ToolModel`)·`guidePaths?`·`introTutorial?`, `ModeTraits.showTool` 삭제. `Game.tools = [knife, scalpel, cutter]`와 `updateToolVisibility()`. 4. `59990f7` 칼 모드 변형 협력 객체 `src/core/knife/SplitShares.ts`(반 가르기 몫 라벨)·`RecutFocus.ts`(다시 자르기 포커스·흐림)·`PlateSpinner.ts`(회전판 회전 시계·각도). 본문은 `Game`에서 그대로 옮겼다. 5. `5591ebe` 칼 모드를 `src/core/modes/KnifeMode.ts`(`GameMode`)로 이전하고 `LegacyMode`를 삭제했다. 식칼·메스 연출은 `src/core/knife/KnifeCutFx.ts`·`ScalpelCutFx.ts`. 새 선택 훅 `plateOutline?`·`onResultLeave?`, `CameraFit.focus`, `ModeHost.render.fitCamera()`, `ModeDebug`에 `pieceShares`·`pieceDim`·`spinning`·`setPlateFrozen`·`groovePoints`. 레지스트리는 `new ModeRegistry([stamp, swipe, slow, knife], knife)`. `Game`의 칼 모드 필드·메서드(`onStrokeEnd`·`presentCut`·`presentScalpelCut`·`focusRecut`·`revealShares`·`tutorialClock` 등)와 `get tool()`이 없어졌다. 6. `0b832a9` 레지스트리 테스트의 fallback 단언(`canDraw` true·`materialOptions` undefined)을 칼 모드로 되살리고 테스트 이름을 실제 검사에 맞췄다(Task 4 리뷰 지적 1회 수정). 7. `8abfeb7` `SlowMode`의 칼 자세·쌓임·HUD 막대를 `src/core/slow/SlowKnifeRig.ts`·`SlowStack.ts`·`SlowBarsView.ts`로 나눴다(본문 그대로, 4단계 미룬 지적 "SlowMode가 큰 파일"). 8. Task 6(테스트 파일 정리 범위)은 작업 없이 결정만 계획에 적었다: 칼 모드 E2E 파일은 각각 한 주제라 나누지 않고 `tests/LevelRules.test.ts` 나누기는 7단계로 둔다. - **기준선 대조 결과**(기준선 `9908c1a`, 단계 시작 `852b159`. 표는 `refactor-baseline.md` 11절): - **출발점(Task 0, `852b159` + 계획 문서)**: 전체 E2E 839 통과 / 7 실패 / 27 건너뜀(19.7분). 실패는 `audio.spec.ts` 6개 + desktop-webkit `slow-food.spec.ts` 레벨 10 화면 여백 20px(`bounds.top` 17.39 < 20) 1개였다. 그 파일은 WebKit에서 혼자 16/16 통과해 부하 흔들림으로 판단했다(리팩토링 전 코드에서 처음 본 것). - **단위 테스트**: 4단계 끝 97파일·1637개 → **101파일·1680개** 통과(새 테스트 `PieceSplitter`·`KnifeVariants`·`KnifeMode`·`SlowParts`와 `ModeRegistry`·`StampMode`·`SwipeGameMode`·`SlowGameMode` 보강). `showTool` 단언은 같은 뜻의 `tool(level)` 단언으로 옮겼고 단언을 지운 테스트는 없다. `npm run build` 통과. - **기능 E2E(`8abfeb7`, 세 프로젝트 전체, 20.7분)**: **840 통과 / 6 실패 / 27 건너뜀**으로 기준선(840/6/27)과 같았다. 실패 6개는 모두 기존 `audio.spec.ts`다. 칼 모드 E2E 12개 파일(`5591ebe`, 세 프로젝트)은 작업 중 443 통과 / 0 실패 / 1 건너뜀이었다. - **성능 E2E**: 칼 모드 이전 직후(`852b159` → `5591ebe`, 같은 시각 번갈아 3회, 후보 8회까지) **회귀 0**. 3회 표 후보 desktop `prepMaxMs cpu4x`(+1.40)·mobile `level37 tomato recut strokeMaxMs`(+1.10)는 8회에서 같았고, 8회 표의 mobile `prepMaxMs free`(+1.15)는 8회 더 재측정해 4.65 → 4.50으로 같았다. 단언 넘김은 양쪽에서 나온 알려진 예산 경계(45 stamp `prepMaxMs ≤ 8`, 28 guitar `strokeMaxMs ≤ 16`)다. 단계 전체(`852b159` → `8abfeb7`, 천천히 썰기 포함, 같은 시각 번갈아 3회 + 후보 4개 11회 + desktop `21→22 dropMaxGapMs` 세 지점 9회): **회귀 0**. 1차 후보 네 개(desktop `21→22 dropMaxGapMs`·`45→46 loadMs`·`level37 recut prepMaxMs`, mobile `level24 hammer strokeMaxMs`) 중 셋은 11회에서 판정선 안, `21→22 dropMaxGapMs`는 세 지점(`852b159`/`0b832a9`/`8abfeb7`) 중앙값 17.60 / 19.90 / 19.10으로 판정선(3.52ms) 안이었다. head에서만 나온 단언 넘김은 desktop `21→22 loadMs` 16.3(1회)·`dropMaxGapMs` 34.8·39.2(아래 관찰의 높은 갈래와 같은 현상으로 봄)와 mobile 21레벨 `prepMaxMs free` 8.8이다. 천천히 썰기 4→5·level 10(`dragBuilds` 0)·휘둘러 베기·틀 찍기도 같음. 표는 `refactor-baseline.md` 11.3·11.4절. - **`Game`에 남은 모드 분기(명시적 예외 목록)**: 시작 배지 `levelBadge` 호출(레벨 정의를 보는 순수 함수), 디버그 노출 `mode: () => level.mode`·`tool: () => level.tool`, 입력 장치 라우팅의 `traits.input` 비교 세 곳. 이것이 전부다(4단계 목록의 튜토리얼 `intro` 목록·`tutorialPath` 제외·기본 가이드 손가락 분기·가이드 원천·칼 모드 경로·`tool`별 칼 표시는 훅으로 옮겼다). 가이드 원천은 모드의 `guidePaths?`가 없으면 `Game`이 `guidePaths(level)`를 쓰는 기본값이다. 새 모드가 이 목록을 더 늘리지 않게 한다. - **미룬 사소한 지적(작업별 리뷰 통과, 고치지 않고 다음 계획으로)**: - **5단계에서 나온 것**: - `PieceSplitter` 테스트 보강 후보(리뷰 M1-2~4, 사소). 계획 문구와 달리 `swap` 직후의 `squash`는 0.4 그대로이고 `impulse(-1.2)`는 속도(`vSquash`)에만 더해져, 테스트는 `step` 한 번 뒤의 `squash < 0.4`로 확인한다. - `SplitShares`는 가짜 `ShareLabels`로만 단위 테스트한다(실제 DOM 라벨은 E2E 몫). - Task 4 리뷰 minor: `sweepPieces`의 "옛 `Game` 호출과 같은 조각 배열" 단언이 `not.toBe`(새 배열임)로만 남았고, RF5(장치 취소 → 처리기 취소 → `exit` → `enter` 순서로 칼자국을 정리하는 것)의 순서는 단위로 검사하지 않는다(E2E가 지킨다). 레지스트리 테스트의 fallback 단언은 레지스트리의 fallback 객체가 아니라 따로 만든 `KnifeMode` 인스턴스를 검사한다. 옛 `fallback.debug() == {}` 단언은 `KnifeMode.debug()`가 비어 있지 않아 뺐다. - 동작 보존 메모(허용한 차이): 식칼 연출이 `KnifeCutFx.present`(async)를 한 번 더 거쳐 마이크로태스크가 한 단계 늘었다(프레임·트윈 순서 영향 없음). 회전판 가드(메스 + 회전판 예외)가 `loadLevel`의 `plate.setLevel` 자리(`mode.plateOutline`)로 옮겨졌다(실제 레벨은 `LevelRules`가 걸러 닿지 않음). - `SlowBarsView`의 `cuts` 캐시, `SlowParts` 테스트가 `pose`·`tip` 등을 직접 검증하지 않는 것(칼 자세·쌓임은 `SlowGameMode`·E2E가 지킴). - **관찰(회귀 아님)**: desktop `21→22 dropMaxGapMs`의 높은 갈래(27~39ms)가 칼 모드 이전 뒤 더 자주 나온 것처럼 보인다(합친 23회 중 base 약 5회, 이전 뒤 약 11회). 세 지점 중앙값은 17.60 / 19.90 / 19.10으로 판정선 안이다. 실기기와 6단계 A/B에서 다시 볼 것. - (최종 리뷰 F2) 메스 + 회전판 가드가 `plate.setLevel` 자리로 늦어져, 던지면 공통 정리가 끝난 반쯤 정리된 상태가 된다(실제 레벨은 `LevelRules`가 걸러 닿지 않음). 6단계에서 `loadLevel` 첫머리에 `next.plateOutline?.(level)`을 불러 이전처럼 정리 전에 던지게 복원할 수 있다. - (최종 리뷰 F3) `SlowMode`의 테스트 호환용 private 접근자(`slowMoving`·`slowDebrisPoint`·`slowPlane`)와 `rig.*` 직접 접근이 섞여 있다. 6단계에서 테스트를 `rig`로 옮기고 접근자를 지운다. (`SlowStack`의 `SLOW_CRUSH_TAU` export는 최종 수정에서 제거했다.) - 출발점 WebKit `slow-food.spec.ts` 레벨 10 화면 여백(17.39 < 20) 부하 흔들림은 최종 전체 실행에는 나오지 않았다. 다시 나오면 부하 조건을 먼저 본다. - **4단계에서 넘어온 것 중 아직 열린 것**: - `SlowCut` 서브스텝 몰림 민감성(실기기 미확인): 프레임이 끊기는 기기에서 부드러운 끌기가 `jerky`로 읽힐 수 있다. 실기기 체크리스트에서 볼 것. - 디버그 `slowVisual()`의 `plane`·`generation`은 slow가 아닌 레벨에서 고정 진단값(plane `{ originY: 0, slopeZ: 0 }`·generation 0)이다(옛 코드는 마지막 slow 레벨의 값·로드 카운터였다). - 입력 장치 취소 중복: `SwipeMode.exit`·`cancelInput`의 `recorder.cancel()`은 `Game.cancelDevices()`가 이미 취소한 뒤라 중복이다(6단계 정리 후보). - `ModeHost.ui.setSlow`는 `SwipeMode`도 준비 실패 안내 패널을 지우는 데 쓴다. 이름이 천천히 썰기에 묶여 있으니 UI 단계(스펙 6.5절)에서 중립 이름으로 바꾼다. - mobile-chromium `home.spec.ts`의 천천히 썰기 레벨 History 재플레이 테스트가 4단계 전체 실행에서 한 번 시간 초과로 실패했다(혼자 12/12, `--repeat-each=10` 10/10). 테스트 타이밍 결함 후보이고 고치지 않았다. - `Game`의 `this.mode.stroke!()`·`depth!()`·`swipe!()` 비null 단언은 `traits.input` 검사 뒤라 비지 않지만 방어 정리 후보다(6단계). - `finishLevel`의 `stamp: false, slow: undefined, swipe: undefined` 기본 키는 `ResultScreen`이 키 집합을 그대로 받게 남긴 과도 상태다(UI 단계). - 3단계에서 넘어온 것: `removeRemainder`의 `pieces.remove(0, false)` 주석, `ProgressService`(던지는 저장소 테스트·`levelCount`와 `LEVELS` 혼용·`progress.progress` 이름), `Game`의 `haptics`·`audio` 직접 사용, `FeelService.hitStop`과 `SwipeMode`의 움직임 줄이기 이중 검사, `e2e/helpers.ts` ↔ `e2e/helpers/*.ts` 순환 import·지역 도우미 복제, 모바일 perf 45 `prepMaxMs`·25/28 `strokeMaxMs`의 예산 경계 흔들림(5단계 A/B에서도 양쪽에 나옴). - 4단계에서 넘어와 5단계에서 해결: `Game.frame` 튜토리얼의 빈 `if` 블록(`05ccb77`에서 `tutorialOn && canDraw()` 한 분기로 정리), `SlowMode` 비대(`8abfeb7`), `prepMaxMs` 갱신 중복(`5591ebe`)과 `Game` 생성자의 어색한 모드 생성 주석(5단계 `05ccb77`·`5591ebe`에서 정리). - **다음 계획(당시): 6단계 `Game` 마무리·6.5 UI 모드별 표시 — 완료(위 6·6.5단계 절). 7 테스트·E2E 정리, 8 문서는 남음.** 6단계: 위 예외 목록 정리(`levelBadge` 호출 위치, 디버그 노출 방식), 비null 단언·장치 취소 중복 정리, `Game`에 남은 칼 모드 보조(`SpinPlate`·`StrokeTrail`·`ShareLabels` UI) 위치 판단. 6.5단계: HUD·결과 카드·시작 배지·준비 패널의 모드별 표시를 모드가 내게(`setSlow` 이름 등). 7단계: `tests/LevelRules.test.ts` 모드별 나누기 등 테스트·E2E 정리. 8단계: 문서. 계획 문서는 단계마다 새로 쓴다(`superpowers:writing-plans`). 이번 단계는 최종 독립 리뷰(Opus / medium) 뒤 사용자 지시로 머지한다. ## R 구조 정리 4단계: 천천히 썰기 모드 이전 (2026-10-23, 브랜치 `refactor/phase4-slow-mode`, main 머지 `852b159`) - 스펙: `docs/superpowers/specs/2026-10-23-oop-refactor-design.md`, 계획: `docs/superpowers/plans/2026-10-23-oop-refactor-phase4.md`, 기준선·4단계 A/B 표: `docs/superpowers/refactor-baseline.md`(9·10절). 구조는 `AGENTS.md`의 '서비스와 모드 객체'·'천천히 썰기 (G)' 절. **동작은 바꾸지 않았다**(레벨 1~58의 규칙·수치·UI 문구, `__jellyDebug` 키·반환 모양. 값까지 같다는 뜻은 아니다: slow가 아닌 레벨의 `slowVisual().plane`·`generation`만 고정 진단값으로 바뀌었다, 아래 미룬 지적). 시작은 main `f4d3e02`. - **한 일**(커밋 순): 1. `1710121` 계획. `676d7b6` WebKit `꾸준히 끌면 ★★★` 흔들림 조사(아래). 2. `baf32bf` 입력 장치 취소 일원화와 다음 판 준비 계약: `Game.cancelDevices()`(`DepthDrag` → `StrokeRecorder` → `SwipeRecorder`)를 `loadLevel`의 `mode.exit()` **앞**과 `cancelInput`에서 부른다("처리기는 `exit` 뒤에도 안전" 규칙 삭제). `prepareNext`·`dropNext`·`preparedId`·`idleFrame`을 `prep?: ModePrep`(`replacesTable`·`prepare(level, stale)`·`drop`·`id`·`tick`)로 묶었다. `JellyDebug` 타입을 `src/core/JellyDebug.ts`로 옮겨 `GameMode.ts` ↔ `Game.ts` 순환을 끊었다. 3. `4b4132a` 성질 속성화: `ModeTraits`(`input`·`tablePieces`·`showTool`·`inputPlaneY`·`shakeWhileReady`·`landShake`·`suppressMotion`·`judgeBelow`·`landingHud`)와 `hint(level)`, 카메라 훅 `camera?(ctx)`(옛 `fitScale`·`fitCamera` 통합), 메인 복귀 `menuReturn?()`(옛 `reloadOnMenu`). 4. `69821fc` `ModeHost` 일원화(평면 `hud`·`setHint`·`updateHud`·`hideTutorial`·`addPrepMs`·`addStrokeMs` 삭제 → `measure`·`ui`·`render.canvas`)와 표시 데이터 훅 `result?`·`resultExtras?`·`hudInfo?`. `Game`은 `SwipeMode`를 생성자에서 만들 뿐 구체 타입을 참조하지 않는다. 5. `13e3498` 협력 객체: `src/core/PieceOps.ts`, `src/core/slow/SlowScene.ts`(장면 자원 지연 생성·데우기·섞임)·`SlowFeelLoop.ts`(소리 고리·진동 펄스), `SlowStage`·`SlowMarks`·`SlowCutPreview`를 `src/core/slow/`로 옮김, `FeelService.slicing`·`stopVibration`. 6. `cd612e7` 천천히 썰기를 `src/core/modes/SlowMode.ts`(`GameMode`)로 이전. `Game`의 천천히 썰기 필드·메서드·`slowing` getter·`resetSlow`·`slowCamera`가 없어졌다. 새 선택 훅 `sceneBlend?(k)`·`onVibrationOff?()`, `ModeHost`에 `sceneBlend()`·`prebuildParts(parts, shape?, stale?)`·`swapSplits`. `LegacyMode`는 칼 전용(성질 상수, deps `{ stroke, pieces }`). 7. `01fdfe8` `e2e/slow.spec.ts`를 `e2e/slow-view.spec.ts`·`slow-flow.spec.ts`·`slow-judge.spec.ts`·`slow-feel.spec.ts`로 나누고(테스트 이름·단언 그대로, `--list` 전후 907개 동일) 도우미를 `e2e/helpers/slow.ts`로 옮겼다(`e2e/helpers.ts`가 다시 내보냄). 8. `2cd7b25` 단계 A/B에서 mobile 4 → 5 `loadMs`가 판정선을 넘어, `SlowMode.enter`의 첫 칼 자세를 옛 `resetSlow`처럼 받침 조각 없이 계산하게 고쳤다(아래). - **기준선 대조 결과**(기준선 `9908c1a`, 단계 시작 `f4d3e02`): - **단위 테스트**: 3단계 끝 94파일·1595개 → **97파일·1637개** 통과(새 테스트 `SlowGameMode`·`SlowFeelLoop`·`PieceOps`와 `ModeRegistry`·`StampMode`·`SwipeGameMode`·`FeelService` 보강, 지운 테스트 없음). `npm run build` 통과. - **기능 E2E(최종 리뷰 수정 뒤 전체 실행)**: 코드는 `dce4328`과 같고 문서 수정만 더한 HEAD에서 세 프로젝트 전체를 다시 돌려(19.5분, 성능 제외) **840 통과 / 6 실패 / 27 건너뜀**으로 기준선(840/6/27)과 같았다. desktop-chromium 287/2/2, mobile-chromium 277/2/12, desktop-webkit 276/2/13. 실패 6개는 모두 기존 `audio.spec.ts`(프로젝트당 2개)이고 WebKit "browser has been closed"·`page.goto` 시간 초과, `home.spec.ts` 재플레이 시간 초과는 이번에는 나오지 않아 다시 실행할 것이 없었다. 단위 1637개와 `npm run build`도 통과. - **기능 E2E**(`01fdfe8`, 세 프로젝트, 20.8분; 이전 실행 기록): **839 통과 / 7 실패 / 27 건너뜀**. desktop-chromium 287/2/2, mobile-chromium 276/3/12, desktop-webkit 276/2/13. 실패 6개는 기준선과 같은 `audio.spec.ts`이고, 더 실패한 1개(mobile-chromium `home.spec.ts`의 `History에서 레벨 5(천천히 썰기)를 재플레이하고…`)는 `slowSliceAll` 대기 시간 초과(실패 순간 화면은 이미 재플레이 완료 뒤 메인, 레벨 5 ★★★ 기록)라 타이밍 흔들림으로 판단했다. 그 파일 혼자 12/12, 그 테스트 `--repeat-each=10` 10/10 통과. desktop-webkit `transition.spec.ts`(3단계부터 넘어온 확인 항목)는 전체 실행 9/9, 혼자 9/9 통과. 이번 전체 실행에는 WebKit "browser has been closed"·`page.goto` 시간 초과가 없었다(`cd612e7` 작업 중 전체 실행에서는 23개가 나왔고 다시 실행하면 통과했다). `2cd7b25` 뒤 천천히 썰기 관련 8개 파일 × 세 프로젝트 205 통과 / 0 실패 / 2 건너뜀. - **성능 E2E**(`f4d3e02`와 같은 시각 번갈아 데스크톱·모바일 `--no-deps`, 판정선 초과로 6회까지): `01fdfe8`에서 후보는 desktop 21→22 `dropMaxGapMs`(두 갈래 흔들림)와 **mobile 4→5 `loadMs` 1.45 → 2.60**이었다. `2cd7b25`로 다시 6회 재서 **회귀 후보 0**: 4→5 `loadMs` desktop 1.85 → 2.35·mobile 1.90 → 2.55, 4→5 `dropMaxGapMs` 17.95 → 18.05·17.70 → 18.45, level 10 다섯 칼질 `dragBuilds` 0. 표는 `refactor-baseline.md` 10절. - **4 → 5 `loadMs` 판단**: `SlowMode.enter`가 `loadLevel`의 테이블 조각 생성 뒤에 불려 첫 칼 자세가 받침 96점을 계산했다(옛 `resetSlow`는 조각 생성 전이라 받침 없음). 계획에 정해 둔 결정 규칙대로 받침 없이 계산하게 바꿨다(받침 자세는 다음 프레임 `slowFrame`이 정하고, 첫 자세의 칼 위치는 받침 유무와 같았다: 단위 테스트가 두 경우의 `setSlicePose` 인자를 비교). - **WebKit `꾸준히 끌면 ★★★` 조사(`676d7b6`)**: 원인은 테스트 입력 결함이다. 이동 한 번이 깊이 약 0.027인 덩어리 입력이 WebKit의 불규칙한 프레임(짧은 프레임·멈춘 프레임에 이동이 몰림)과 만나 `SlowCut`의 서브스텝 배분에서 눌림 증가율이 `JERK_A`를 넘었다. 그 테스트만 `steps: 45`로 나눠 보내 10/10 통과(WebKit `--repeat-each=10`, 워커 1·3). 제품 판정 규칙은 스펙대로라 바꾸지 않았다. 자세한 기록은 `refactor-baseline.md` 9절. - **`Game`에 남은 모드 분기(5단계·UI 단계 스펙 6.5절 후보)**: 시작 배지 `levelBadge`(레벨 정의를 보는 순수 함수), 튜토리얼 대상 `intro` 목록(`tutorialOn`: `straight`·`curve`·`stamp`·`slow`·`swipe`), `tutorialPath`의 틀 찍기·휘둘러 베기 제외(`level.mode === 'swipe'` 지역 변수 포함), `frame`의 기본 가이드 손가락 분기 `level.mode !== 'stamp'`, 가이드 원천(`loadLevel`: 틀 찍기는 `session.guides`), 칼 모드 경로(`level.recut`·`level.spin`·`level.mode === 'split'`·`tool`별 칼 표시, 디버그 `pieceShares`). 입력 장치 라우팅의 `traits.input` 비교 세 곳(`StrokeRecorder.canAccept`·`DepthDrag.canStart`·`SwipeRecorder.canStart`)은 장치 연결 규칙이라 분기로 치지 않는다(`cd612e7` grep 예외로 인정). - **미룬 사소한 지적(작업별 리뷰 통과, 고치지 않고 다음 계획으로)**: - **`SlowCut` 서브스텝 몰림 민감성(실기기 미확인)**: 한 프레임에 몰린 입력을 그 프레임의 서브스텝에 나누는 방식이라, 프레임이 끊기는 기기(긴 프레임 뒤 짧은 프레임, 멈춘 프레임에 이동 여럿)에서는 부드러운 끌기가 `jerky`로 읽힐 수 있다. WebKit 조사에서 테스트 입력으로만 확인했고 실기기 입력이 고르게 온다는 것은 확인하지 않았다(기준선 9절의 단정 표현을 이번에 고침). 실기기 체크리스트에서 볼 것. - 디버그 `slowVisual()`의 `plane`·`generation`은 slow가 아닌 레벨에서 고정 진단값(plane `{ originY: 0, slopeZ: 0 }`·generation 0)이다. 옛 코드는 `Game` 필드 값을 그대로 보여, plane은 마지막 slow 레벨의 값을 그대로 유지했고 generation은 `loadLevel`마다 느는 로드 카운터였다. 키와 모양(타입)만 같고 값은 다르다(`e2e/debug-api.spec.ts`는 키 고정만 보고 통과, E2E의 `slowVisual()` 사용처는 이 두 값을 단언하지 않는다). - 입력 장치 취소 중복: `SwipeMode.exit`·`cancelInput`의 `recorder.cancel()`은 `Game.cancelDevices()`가 이미 장치를 취소한 뒤라 중복이다(해롭지 않음). 6단계 정리 후보. - `ModeHost.ui.setSlow`는 `SlowMode`의 천천히 썰기 패널 말고 `SwipeMode`도 준비 실패 안내 패널을 지우는 데 쓴다. 이름이 천천히 썰기에 묶여 있으니 UI 단계(스펙 6.5절)에서 준비 패널 같은 중립 이름으로 바꾼다. - mobile-chromium `home.spec.ts`의 천천히 썰기 레벨 History 재플레이 테스트가 전체 실행에서 한 번 시간 초과로 실패했다(그 파일 혼자 12/12, 그 테스트 `--repeat-each=10` 10/10). 테스트 쪽 타이밍 결함 후보이고 고치지 않았다. - `Game`의 `this.mode.depth!()`·`swipe!()`·`stroke!()` 비null 단언: `traits.input` 검사 뒤라 비지 않지만 방어 정리 후보다(6단계). - `src/core/modes/SlowMode.ts`가 큰 파일이 됐다(칼질 진행·칼 자세·미리 만들기·바닥 통과·쌓임·파편·HUD가 한 클래스). 칼 자세·쌓임·HUD 막대를 협력 객체로 나누는 것은 5단계 후보다. - `finishLevel`의 `stamp: false, slow: undefined, swipe: undefined` 기본 키는 `ResultScreen`이 키 집합을 그대로 받게 남긴 과도 상태다(UI 단계). - 3단계에서 넘어온 것 중 남은 것: `removeRemainder`의 `pieces.remove(0, false)` 주석, `ProgressService`(던지는 저장소 테스트·`levelCount`와 `LEVELS` 혼용·`progress.progress` 이름), `Game`의 `haptics`·`audio` 직접 사용, `FeelService.hitStop`과 `SwipeMode`의 움직임 줄이기 이중 검사, `e2e/helpers.ts` ↔ `e2e/helpers/*.ts` 순환 import·지역 도우미 복제, 모바일 perf 45 `prepMaxMs`·25/28 `strokeMaxMs`의 예산 경계 흔들림(이번 A/B에서도 양쪽에 나옴). - 3단계 최종 리뷰의 Task 0 목록과 `GameMode` 비대 후속은 이번 단계에서 끝났다(위 3단계 절 표시). `Game.frame` 튜토리얼의 빈 `if` 블록(모드가 그렸으면 아무것도 안 함)은 주석으로 남아 있다. - **다음 계획(당시): 5단계 칼 모드 이전 — 완료(위 5단계 절).** `Game`의 획 처리 구역(`onStrokeEnd`·`presentCut`·`presentScalpelCut`, 다시 자르기 포커스·흐림, 회전판, 반 가르기 몫 라벨)과 가이드 원천을 모드 객체(식칼·메스·반 가르기·다시 자르기·회전판, 변형은 협력 객체)로 옮기고 `LegacyMode`를 없앤다. 함께 볼 것: 위 남은 분기(튜토리얼 `intro` 목록·`tutorialPath` 제외·가이드 원천), `SlowMode` 나누기. 이어서 `Game` 마무리(6단계: 모드 분기 0, 비null 단언 정리), UI 모드별 표시(6.5단계), 테스트·E2E 정리(7단계), 문서(8단계). 계획 문서는 새로 쓴다(`superpowers:writing-plans`). 이번 단계는 최종 독립 리뷰(Opus / medium) 뒤 사용자 지시로 머지한다. ## R 구조 정리 3단계: 휘둘러 베기 모드 이전 (2026-10-23, main 머지 `aaed554`) - 스펙: `docs/superpowers/specs/2026-10-23-oop-refactor-design.md`, 계획: `docs/superpowers/plans/2026-10-23-oop-refactor-phase3.md`, 기준선·측정 소음·성능 판정 규칙·3단계 A/B 표: `docs/superpowers/refactor-baseline.md`(8절). 구조는 `AGENTS.md`의 '서비스와 모드 객체'·'휘둘러 베기 (H)' 절. **동작은 바꾸지 않았다**(레벨 1~58의 규칙·수치·UI 문구, `__jellyDebug` 키·타입). - **한 일**(커밋 순): 1. `12b8b95` 입력 처리기 일반화와 `exit` 순서: `GameMode`에 `InputKind`(`'stroke'`|`'depth'`|`'swipe'`)와 `input(level)`, 처리기 `stroke?()`·`depth?()`·`swipe?()`(`StrokeHandlers`·`DepthHandlers`·`SwipeHandlers`)를 들이고 `ownsStroke?`·`onPress?`를 없앴다. `Game`은 세 장치를 지금 모드의 `input(level)`이 맞을 때만 열고 콜백을 그 처리기로 넘긴다. `loadLevel`은 `mode.exit()`를 공통 정리 앞에서 부른다(처리기는 `exit` 뒤에도 안전해야 한다는 규칙을 `GameMode.exit` 주석과 AGENTS.md에 적음). 0~2단계 최종 리뷰의 설계 결정 두 가지가 이로써 해결됐다. 2. `d9a86f5` 판 끝 전환·다음 판 준비 성질: `scene(level)`·`sweepPieces()`·`landShake()`와 선택 `prepareNext?`·`dropNext?`. `Game.prepareLevel`·`dropPrepared`·`sweepAndDrop`이 모드에 묻는다. 3. `7ebd866` 옛 `src/core/SwipeMode.ts`를 `src/core/swipe/`의 `SwipePrep.ts`(미리 만들기)·`SwipeEngine.ts`(몸체·판정·교체, 클래스 `SwipeMode` → `SwipeEngine`)·`SwipeDebug.ts`(디버그 타입·`tumbleOf`)로 나눔(본문 변경 없음). 4. `c2b9d65` 휘둘러 베기를 `src/core/modes/SwipeMode.ts`(`GameMode`)로 이전. `Game`의 휘둘러 베기 동작 코드(`swipeFrame`·`endSwipe`·타격 묶음·셰이더 데우기·메인 미리보기·튜토리얼 손가락·카메라 맞춤·다음 판 준비 인계)가 모드로 갔고, `Game`에는 `swipe` 필드·`swiping` getter와 UI 분기만 남았다. 새 선택 훅 `tutorial?`·`fitCamera?`·`onTierChange?`·`shakeWhileReady?`와 브리프 밖 훅 `idleFrame?`·`reloadOnMenu?`·`preparedId?`·`onResize?`, `ModeHost.render`·`ui` 묶음, `FeelService.hitStop`·`resetLevel`, `ModeDebug`에 swipe 키 4개, `ModeRegistry.all()`. 5. `cb5c6d1`·`b80c3fe` 테스트 정리: `e2e/swipe.spec.ts`를 `e2e/swipe-flight.spec.ts`·`e2e/swipe-pieces.spec.ts`·`e2e/swipe-life.spec.ts`로 나누고(테스트 이름·단언 그대로, `npx playwright test --list` 전후 907개 동일) 도우미를 `e2e/helpers/swipe.ts`로 옮겨 `e2e/helpers.ts`가 다시 내보낸다. `StampMode` 틀 조각 재질이 `materialOptions`를 거치고, `installDebug`가 모드별 `debug()`를 캐시한다(`modeDebug`). 단위 테스트 보강: `tests/SwipeGameMode.test.ts`(핸들·입력 종류·타격 묶음·`exit`·`takeSwipePrep` id 일치/불일치·`preparedId`·`fitCamera` home·`idleFrame` 측정 제외·`reloadOnMenu`), `tests/FeelService.test.ts`(`hitStop`·`resetLevel`), `tests/GameClock.test.ts`, `tests/StampMode.test.ts`. - **기준선 대조 결과**(기준선 `9908c1a`, 3단계 시작 `562d80d`, 끝 `b80c3fe`): - **단위 테스트**: 기준선 87파일·1534개 → 0~2단계 93파일·1572개 → 3단계 끝 **94파일·1595개** 통과(새 테스트가 늘어난 것, 지운 테스트 없음). `npm run build` 통과. - **기능 E2E**(세 프로젝트 전체 실행은 `c2b9d65`에서, 19.2분): 합계 **840 통과 / 6 실패 / 27 건너뜀**으로 기준선(840/6/27)과 같고 실패 6개는 모두 `audio.spec.ts`(프로젝트당 2개, 기존)다. 중간 실행: `12b8b95` desktop-chromium 287/2/2, mobile-chromium 277/2/12, desktop-webkit 275/3/13(WebKit `slow.spec.ts`의 `꾸준히 끌면 ★★★` 1개가 더 실패, 아래 항목), `d9a86f5` transition·slow·swipe·progress·context-loss·stamp·debug-api × 세 프로젝트 371 통과 / 0 실패 / 4 건너뜀. `b80c3fe`(테스트 분리 뒤)는 휘둘러 베기 세 파일 × 세 프로젝트 115 통과 / 0 실패 / 2 건너뜀(태블릿, 데스크톱 전용)과 `stamp.spec.ts`·`debug-api.spec.ts` desktop-chromium 23 통과만 확인했다. 3단계 마지막 커밋(`7d9e4de`, 문서 수정 뒤 코드는 `b80c3fe`와 같음)에서 세 프로젝트 전체 기능 E2E를 다시 돌려 **840 통과 / 6 실패 / 27 건너뜀**(19.3분, 실패는 audio 6개뿐, WebKit `꾸준히 끌면 ★★★` 통과)으로 기준선과 같았다. 성능 E2E는 `c2b9d65`의 A/B 뒤로 다시 돌리지 않았다(그 뒤 변경은 E2E 분리·`StampMode` 재질 경로·`installDebug` 캐시와 문서). - **성능 E2E**(같은 시각 직전 커밋과 번갈아 데스크톱·모바일 `--no-deps` 각 3회, 판정선 초과 지표는 재측정): `562d80d`→`12b8b95`, `12b8b95`→`d9a86f5`, `7ebd866`→`c2b9d65` 모두 **회귀 없음**(중앙값이 max(1ms, 20%) 이상 나빠진 지표 없음). `meshBuildsDuringSlash`·`dragBuilds`는 모두 0, 52·54·58 `prepMaxMs`는 5.9~6.3(예산 ≤ 8). 판정선을 한 번 넘은 항목(데스크톱 25·26 반 가르기 `strokeMaxMs`, 모바일 26 `strokeMaxMs`, 모바일 24 hammer·58 `strokeMaxMs`)은 이번 변경이 닿지 않는 경로이고 번갈아 3회 재측정에서 모두 "같음"(차이 -1.4~+1.0ms)이었다. 가장 신경 쓴 45 → 46 `loadMs`: 데스크톱 3.10 → 3.90(+0.80, 1ms 아래), 모바일 2.90 → 2.50, `9908c1a` 2.00(데스크톱)·2.60(모바일). 기준선 `9908c1a` 같은 시각 1회 측정과도 모든 지표가 `7ebd866`·`c2b9d65` 범위 안이다. 표는 `refactor-baseline.md` 8절. - **`Game`에 남은 모드 분기(UI 단계 스펙 6.5절·5단계 후보)**: - 휘둘러 베기: 칼 모델 숨김(`loadLevel`의 `knife.setVisible(… && !swipe)`와 메뉴 복귀의 칼 표시)·비행 평면 높이(`sceneMgr.setSurfaceHeight(swipe ? SWIPE_FLY_Y : surface)`)·튜토리얼(`tutorialOn`의 `intro === 'swipe'`, `tutorialPath` 제외, 기본 손가락 분기의 `!swiping`)·힌트 `SWIPE_HINT`·결과 카드와 HUD의 휘둘러 베기 표시(`finishLevel`·`updateHud`가 `this.swipe.session`·`loaded`를 읽음. `Game`이 `SwipeMode` 구체 타입을 필드로 들고 있음)·`loadLevel`의 테이블 조각 만들기 분기(`swipe ? [] : this.session.pieces`). - 틀 찍기: 0~2단계 절에 적은 목록 그대로(튜토리얼 손가락 3곳·결과 카드 `stamp` 플래그·HUD `unit`·가이드 원천). - 아직 옮기지 않은 모드: 칼(식칼·메스·반 가르기·다시 자르기·회전판)은 `Game`의 획 처리 구역, 천천히 썰기는 `slowFrame`·`SlowCutPreview` 연결 구역이다. 둘은 `LegacyMode`(입력 판별·처리기·지금 레벨·조각만 받음)가 받는다. - **미룬 사소한 지적(작업별 리뷰 통과, 고치지 않고 다음 계획으로)**: - **`GameMode` 비대(4단계 이후 정리 후보)**: 선택 훅이 늘었다(`tutorial`·`fitCamera`·`onTierChange`·`shakeWhileReady`·`idleFrame`·`reloadOnMenu`·`preparedId`·`onResize`·`prepareNext`·`dropNext` 등). 후속: 다음 판 준비(`prepareNext`·`dropNext`·`preparedId`)를 하위 객체 하나로 묶기, `shakeWhileReady`를 메서드가 아니라 모드의 성질(속성)로 바꾸기, `idleFrame`과 `beforePieces`를 하나로 합치기(둘 다 `SwipeMode`에서 같은 `swipeFrame` 본문), `ModeHost`의 `hud`·`ui` 중복 경로 제거(`hud.showFinger`·`hideFinger`와 `ui.showFinger`·`hideFinger`, `setHint`와 `ui.setHintVisible` 등 평면 멤버를 묶음으로 일원화). - `Game`이 `SwipeMode` 구체 타입을 참조한다(결과 카드·HUD 표시용. UI 단계에서 모드 객체의 표시 데이터로 대체). - `session.pieces` 분기: `loadLevel`이 `swipe ? [] : session.pieces`로 테이블 젤리 유무를 가른다(모드의 성질로 옮길 후보). - `FeelService.hitStop`의 움직임 줄이기 검사와 `SwipeMode.onSwipeHit`의 `reducedMotion` 블록이 이중이다(동작은 같음). - 코드 주석: `SwipeEngine`의 `SwipeHost.onHit`·`prepError`와 `SwipeDebug`의 일부 주석이 옛 구조("Game 이 …")를 말한다. - 테스트·E2E 파일: `e2e/helpers.ts`↔`e2e/helpers/swipe.ts`의 순환 import(호출 시점 참조라 안전. `core.ts` 분리 후보), `e2e/helpers.ts`와 `e2e/helpers/` 공존, `swipe-flight`·`swipe-pieces`·`swipe-life`의 지역 도우미·`beforeEach`/`afterEach` 복제, 새 테스트 일부는 기존 동작을 검증하는 것이라 RED 없이 추가했다. - 0~2단계에서 넘어온 것 중 남은 것: `removeRemainder`의 `pieces.remove(0, false)` 주석, `ProgressService`(던지는 저장소 테스트·`levelCount` 인자와 `LEVELS` 전역 혼용·`progress.progress` 이름), `GameMode.ts`↔`Game.ts`의 `JellyDebug` type-only import 순환(6단계 정리), `Game`의 `haptics`·`audio` 직접 사용(`FeelService`로 모으기), 모바일 perf 45 `prepMaxMs`·25 `strokeMaxMs`의 예산 경계 흔들림. 3단계에서 해결: `GameMode.onPress` 반환형(`onPress`가 인터페이스에서 빠짐), `FeelService.resetLevel()`, `StampMode`의 `materialOptions`와 구멍 부착 단위 테스트, `GameClock` 트윈 완료 순서·다중 완료 테스트, `installDebug`의 `debug()` 캐시. - **WebKit `e2e/slow.spec.ts` `꾸준히 끌면 ★★★`(4단계 `676d7b6`에서 조사·처리: 테스트 입력 결함, `refactor-baseline.md` 9절)**: 3단계 시작 `562d80d`(main)에서도 `--repeat-each=5`에 **5회 모두 실패**했다(`result-legend` `toHaveCount(0)`에서 1, Task 1에서 확인). 반면 `d9a86f5`·`c2b9d65` 전체 실행에서는 통과했다. 이 단계의 변경과 무관하게(같은 `562d80d`에서 실패) 환경·타이밍에 따라 결과가 갈리는 테스트라 단계 판정에서는 제외했다. 천천히 썰기 경로이고 실제 시각 입력과 `setTimeScale(3)`에 기대는 테스트였다. 원인은 4단계 `676d7b6`에서 밝혀 처리했다(테스트 입력 결함: 덩어리 입력을 `steps: 45`로 나눠 보냄. 위 4단계 절의 'WebKit `꾸준히 끌면 ★★★` 조사'와 기준선 문서 9절 참고). 이 항목은 끝났다. - **최종 독립 리뷰(Opus / medium, 2026-10-23): 머지 가능.** 4단계 계획의 **Task 0(인터페이스 정리)** 으로 넣을 것 — **4단계에서 모두 완료**(위 4단계 절: 1·2·7은 `baf32bf`, 3·4·5는 `4b4132a`, 6·8은 `69821fc`, WebKit 조사는 `676d7b6`): 1. **입력 장치 취소 일원화(Important)**: 지금은 `StrokeRecorder`·`DepthDrag`를 `Game`이 공통 정리에서 취소한다. 그런데 `mode.exit()` 뒤라 `DepthDrag.cancel` → `release`가 나간 모드에 닿는다. 천천히 썰기를 옮기면 실제 결함이 된다. `SwipeRecorder`는 모드가 직접 취소한다. - 고칠 것: `loadLevel` 첫머리에서 `mode.exit()` **앞에** 세 장치를 모두 취소하는 `cancelDevices()`를 부른다. `cancelInput`도 같은 함수를 쓰고, "exit 뒤 처리기 안전" 규칙은 없앤다. 2. **다음 판 준비 계약(Important)**: `prepareNext`가 있으면 테이블 준비를 대신한다. 천천히 썰기는 테이블 첫 젤리와 장면 준비가 둘 다 필요하다. - 고칠 것: `prepareNext`·`dropNext`·`preparedId`·`idleFrame`을 `prep?: { prepare; drop; id; tick; replacesTable }` 하나로 묶는다. 3. **바뀌지 않는 성질을 속성(또는 레벨을 받는 함수)으로**: 대상은 `shakeWhileReady`·`landShake`·테이블 젤리 유무·칼 표시·비행 평면 높이·힌트·연출 억제·HUD 단위다. `Game`에 남은 `swiping`·`slowing`·`'stamp'` 분기 절반 이상이 이 성질 조회로 바뀐다. 4. **카메라 훅 하나로**: `fitScale`·`fitCamera`를 `camera?(ctx)`로 합친다. 천천히 썰기 결과 카드 줌 아웃도 여기로 온다. 5. **메인 복귀 정리**: `menuReturn(): { reload }` + `onMenu()`로 바꾼다. `Game`의 `slowCut.depth > 0` 조건을 옮길 자리다. 6. **`ModeHost` 일원화**: 평면 `hud`·`setHint`·`hideTutorial`과 `ui`의 중복을 없애고, 측정은 `measure`로 묶는다. 7. **`JellyDebug` 타입을 `Game.ts` 밖으로**: type-only 순환을 끊는다. 8. **표시 데이터 훅(`resultExtras`·`hudInfo`) 모양**: 4단계에서 `Game`이 구체 모드 타입을 읽는 지점을 늘리지 않게, 모양을 먼저 정한다. - **4단계 시작 전 조사 필수**: WebKit `slow.spec` `꾸준히 끌면 ★★★`의 흔들림. 4단계가 바로 그 경로를 옮기므로, 흔들림이 회귀를 가릴 수 있다. - 사소: `SwipeEngine`·`SwipeDebug`의 "Game 이 …" 주석 3곳, `Game.frame` 튜토리얼의 빈 `else if`, 튜토리얼 시계 두 개, `GameMode.input` 인자 타입(`undefined` 허용), `ModeHost.render`는 생성 시점 참조라는 전제 주석. - **다음 계획(당시): 4단계 천천히 썰기 모드 이전 — 완료(위 4단계 절).** `Game`의 `slowFrame`·`SlowCutPreview` 연결 구역·`onSlowPress`·`onSlowDrag`·`onSlowRelease`를 `src/core/modes/`의 모드 객체로 옮기고(이때 `LegacyMode`의 `depth` 처리기와 입력 판별의 slow 분기가 빠진다), 위 `GameMode` 비대 후속 정리를 함께 본다. 이어서 칼 모드·가이드 원천(5단계), `Game` 마무리(6단계), UI 모드별 표시(6.5단계), 테스트·E2E 파일 정리(7단계), 문서(8단계)다(스펙 6절 단계표). 계획 문서는 새로 쓴다(`superpowers:writing-plans`). 최종 독립 리뷰(Opus / medium) 뒤 머지한다. ## R 구조 정리: 모드별 객체 분리 리팩토링 0~2단계 (2026-10-23, main 머지 `63f2178`) - 스펙: `docs/superpowers/specs/2026-10-23-oop-refactor-design.md`, 계획: `docs/superpowers/plans/2026-10-23-oop-refactor-phase0-2.md`, 기준선·측정 소음·성능 판정 규칙: `docs/superpowers/refactor-baseline.md`. 구조는 `AGENTS.md`의 '서비스와 모드 객체' 절. **동작은 바꾸지 않았다**(레벨 1~58의 규칙·수치·UI 문구, `__jellyDebug` 키). - **0단계 안전망**: 기준선 문서와 `e2e/debug-api.spec.ts`(`__jellyDebug` 키 72개와 `slowSlice()`·`stampMotion()`·`swipe()` 반환 키를 리터럴로 고정). 기준선 측정 중 `e2e/swipe.spec.ts`의 `미리 만든 메시 자원이 없는 결과는…` 테스트가 webkit에서 내던 결정적 실패(`stubVibrate` 누락)를 테스트만 고쳤다(`9908c1a`, 기준선 커밋). - **1단계 서비스 4개**(`src/core/`): `GameClock`(시간·트윈), `FeelService`(슬로모션·흔들림·진동·소리 기록·판정 문구·콤보), `PieceStore`(조각 목록), `ProgressService`(진행·저장). 서비스는 모드를 모른다(천천히 썰기의 슬로모션 제외는 `FeelService.play(feel, suppressMotion)` 인자로 호출하는 쪽이 넘김. 컨트롤러가 수용). - **2단계 모드 객체**: `GameMode` 인터페이스(선택 훅 `onMenu`·`fitScale`·`ownsStroke`·`onPress`, `ModeDebug`)·`ModeRegistry`·`LegacyMode`·`ModeHost`·프레임 훅 4개(`afterGuides`·`beforePieces`·`afterPieces`·`beforeRender`)를 들이고, 첫 모드로 틀 찍기를 `src/core/modes/StampMode.ts`로 옮겼다. `Game.ts`에서 틀 찍기 필드·메서드가 없어졌다. 아직 옮기지 않은 모드는 `LegacyMode`(빈 구현)가 받아 `Game` 코드가 그대로 돈다. - **커밋**: `d9d7c97`(시간·트윈 `GameClock`) → `ec0d3a8`(손맛) → `199cf55`(조각) → `6d6ddc5`(진행·저장) → `c2cc3f5`(GameMode·레지스트리·훅) → `b3c5028`(StampMode) → 문서(이 절). 기준선 대조(2단계 통과 기준, 스펙 7절): - 단위 테스트: 기준선 87파일·1534개 → 93파일·**1572개** 통과(새 단위 테스트가 늘어난 것이고 지운 테스트는 없다). - 기능 E2E 세 프로젝트(`b3c5028` 전체 실행, 19.2분): 839 통과 / 7 실패 / 27 건너뜀. 프로젝트별 desktop-chromium 287/2/2, mobile-chromium 277/2/12(기준선과 같음), desktop-webkit 275/3/13(기준선 276/2/13). 실패는 `audio.spec.ts`의 `절단부터 다음 판까지 선택 WAV만 재생하고…` 테스트 6개(기존)와 **desktop-webkit `slow.spec.ts`의 `꾸준히 끌면 ★★★` 1개**다. 이 1개는 직전 커밋 `c2cc3f5`에서도 `--repeat-each=5`에 3회 실패한 기존 흔들림이라 회귀가 아니다(`refactor-baseline.md` 6절). 건너뜀 27개는 기준선과 같다. - 성능 E2E(desktop·mobile × 같은 시각 직전 커밋과 번갈아 3회, 판정선 초과 지표는 8회): 기준선 절대값은 기기 상태로 20~50% 흔들려(`9908c1a`를 다시 재도 높게 나옴) **같은 시각 직전 커밋 대비 A/B**로 판정했다(컨트롤러 결정). `c2cc3f5`(6d6ddc5 대비)와 `b3c5028`(c2cc3f5 대비) 모두 중앙값이 max(1ms, 20%) 이상 나빠진 지표가 없다. 모든 중앙값은 예산 안이고 `meshBuildsDuringSlash`·`dragBuilds`는 0이다. 단언을 한 번 넘은 경우는 모바일 45 `prepMaxMs`(8.40·8.60)와 25 `strokeMaxMs`(16.30)뿐이고 직전 커밋도 같은 값(8.2·16.3)이라 예산 경계의 흔들림이다. | 지표(중앙값, ms) | 예산 | 기준선 문서 | `c2cc3f5`→`b3c5028` 같은 시각 A/B | 판정 | |---|---|---|---|---| | desktop 45 stamp prep / stroke | 기준선 대비 / ≤16 | 6.60 / 4.10 | 6.80 → 6.60 / 4.10 → 4.10 | 같음 | | desktop 25 watergun stroke | ≤16 | 9.30 | 9.80 → 14.10 (3회), 8회 재판정 12.65 → 13.95 | 두 갈래(≈10 / ≈13.5) 흔들림, 8회 중앙 +1.3ms로 판정선 안 | | desktop 21→22 drop / load | ≤34 / ≤16 | 18.10 / 1.20 | 18.40 → 27.20, 8회 재판정 18.80 → 18.80 / 2.00 → 2.10 | 같음 | | desktop 52·54·58 swipe prep | ≤8 | 5.90·5.90·6.00 | 5.90→5.80, 6.10→6.20, 6.00→6.00 | 같음 | | desktop 10 slow dragBuilds, 52·54 meshBuildsDuringSlash | 0 | 0 | 0 → 0 | 같음 | | mobile 45 stamp prep / stroke | 기준선 대비 / ≤16 | 7.00 / 4.10 | 7.30 → 7.70 / 4.50 → 4.40 (8회 재판정 7.25 → 7.80, +0.55ms) | 1ms 안 | | mobile 25 watergun stroke | ≤16 | 9.80 | 14.60 → 15.30 (8회 재판정 15.55 → 15.25) | 같음 | | mobile 22~28 stroke(전체) | ≤16 | 6.6~10.7 | c2cc3f5 대비 -0.9 ~ +0.9 | 같음 | | 45 → 46 loadMs | ≤16 | 2.00(d) / 2.10(m) | 3.00 → 2.60(d) / 3.30 → 3.70(m) | 같음 | `b3c5028` 전체 A/B 표(지표 40여 개 × 3회)는 `docs/superpowers/refactor-baseline.md` 7절에 있다. - **`Game`에 남은 모드 분기(UI 단계 스펙 6.5절·5단계 후보)**: - 틀 찍기: 튜토리얼 손가락 3곳(`tutorialOn`의 `intro === 'stamp'`, `tutorialPath` 제외 조건, 손가락 분기의 `level.mode !== 'stamp'`), 결과 카드 `stamp` 플래그(`finishLevel`), HUD `unit: 'stamps'`(`updateHud`), 시작 배지 `levelBadge`(레벨 정의를 보는 순수 함수), 가이드 원천(`loadLevel`, `loadLevel`의 `new CuttingGuides(level.mode === 'stamp' ? this.session.guides : guidePaths(level), …)`: 틀 찍기는 `session.guides`, 그 밖은 `guidePaths(level)`. 5단계 항목). - 모드 객체로 옮기지 않은 모드: 칼(식칼·메스·반 가르기·다시 자르기·회전판)은 `Game`의 획 처리 구역, 천천히 썰기는 `Game`의 `slowFrame`·`SlowCutPreview` 연결 구역, 휘둘러 베기는 당시 `SwipeMode.ts` + `Game.swipeFrame` 연결부에 있었다(3단계에서 `src/core/modes/SwipeMode.ts`와 `src/core/swipe/`로 옮김, 위 3단계 절). UI(HUD 단위·결과 카드·메인 메뉴)의 모드 분기는 모든 모드가 걸쳐 있다. - **미룬 사소한 지적(작업별 리뷰 통과, 고치지 않고 다음 계획으로)**: - 기준선 문서의 45레벨 prep "예산 판정 안 함" 서술과 `perf.spec.ts`의 `≤ 8` 단언 구분(`refactor-baseline.md` 6절에서 구분해 적음). - `GameClock`: 트윈 완료가 `splice` 뒤에 불리는 순서와 한 `advance`에서 여러 트윈이 끝나는 경우의 단위 테스트가 없다. - `Game`이 `feel.slowmo.reset`·`judge.hide`를 직접 부른다(`FeelService.resetLevel()` 후보). - `removeRemainder`의 `pieces.remove(0, false)`가 왜 해제하지 않는지 주석이 없다. - `ProgressService`: 던지는 저장소 테스트 없음, `levelCount` 인자와 `LEVELS` 전역 혼용, `progress.progress` 이름 겹침. - `GameMode.onPress` 시그니처가 `void`인데 `StampMode.onPress`는 `Promise`(`void | Promise`로 넓힐 후보). - `StampMode` 틀 조각 재질도 `materialOptions`를 거치게(5단계 후보), `StampMode.enter`의 구멍 부착 단위 테스트, 틀 찍기 안내 문구가 `Game`과 `StampMode`에서 두 번 쓰이는 것(스펙 6.5절). - `GameMode.ts`↔`Game.ts`의 `JellyDebug` type-only import 순환(6단계 정리). - mobile perf 45 `prepMaxMs`·25 `strokeMaxMs`가 예산 경계(위 표 참고). - **최종 독립 리뷰(Opus / medium, 2026-10-23): 머지 가능.** 3단계 계획 첫머리에 정할 설계 결정 두 가지(3단계에서 해결: `exit()`는 공통 정리 앞, 입력은 `InputKind`와 처리기 방식): 1. `mode.exit()` 위치: 지금은 `loadLevel`의 공통 정리(`swipeMode.clear()`·`pieces.clear()`·`clock.clearTweens()`) 뒤에 불린다. 휘둘러 베기는 몸체 해제가 조각 해제보다 먼저여야 한다. 그래서 `exit()`를 공통 정리 앞으로 옮기거나, 정리 앞 훅을 따로 둔다. 2. 입력 장치 소유·연결: `ownsStroke?`·`onPress?`는 틀 찍기 전용 임시안이다. 칼(StrokeRecorder 시작·이동·뗌·취소)·천천히 썰기(DepthDrag)·휘둘러 베기(SwipeRecorder)를 함께 받는 일반 방식을 정한다. `onPress` 반환형 `void | Promise`도 이때 바로잡는다. - 함께 볼 것: - 판 끝 전환 성질(시점·장면 섞임·쓸 조각·착지 흔들림)과 `prepare(nextLevel)`을 `GameMode`에 들인다. - `ModeHost`(멤버 33개)를 UI·전환·장면 서비스로 묶는다. - `FeelService` 감싸기(`hitStop()`·`resetLevel()`)를 정한다. - `haptics`·`audio`의 직접 사용을 `FeelService`로 모은다. - `installDebug`의 `mode.debug()`가 조회할 때마다 객체를 새로 만드니 캐시한다. - WebKit `slow.spec` `꾸준히 끌면 ★★★`의 흔들림은 리팩토링 전 기준선 `9908c1a`에서도 `--repeat-each=5`에 2회 실패했다(최종 리뷰가 확인). - **다음 계획(당시): 3단계 휘둘러 베기 모드 이전 — 완료(위 3단계 절).** 옛 `SwipeMode.ts`를 `GameMode` 구현으로 만들어 `Game.swipeFrame`·`swipeBurst`·입력 연결을 모드 객체로 옮기고, `SwipePrep`·`PrepQueue`·`KidPrep` 정리(스펙 4.4절)를 포함하는 계획이었다. 이어서 천천히 썰기(4단계), 칼 모드·가이드 원천(5단계), `Game` 마무리(6단계), UI 모드별 표시(6.5단계), 테스트·E2E 파일 정리(7단계), 문서(8단계)다(스펙 6절 단계표). 계획 문서는 새로 쓴다(`superpowers:writing-plans`). ## H 휘둘러 베기 레벨 46~58 (2026-10-23, main 머지 `f0d5a47`) - 스펙: `docs/superpowers/specs/2026-10-22-swipe-slicing-design.md`, 계획: `docs/superpowers/plans/2026-10-22-swipe-slicing.md`. 구조·규칙은 `AGENTS.md`의 '휘둘러 베기 (H)' 절. - 내용: 위에서 내려다보는 카메라(`SceneManager.fitBox`)와 놀이 상자, 포물선 발사(46 튜토리얼 멈춤, 47~55 1~3개, 56~58 4조각), 실제 시각 기준 슬래시 속도·넉넉한 판정, 15° 12방향 넓이 중심 절단(이 모드만 가이드 스냅 금지 예외)과 미리 만든 결과 바꿔치기(`SwipePrep`·`PrepQueue`), 4조각 판 조각의 뒤 준비(`KidPrep`), 휘두른 횟수 별(`swipeStars`), 히트스톱·흔들림·진동 `[8]`·`x2` 문구·슬래시 리본·섬광·즙 파편(`SwipeFx`), 메인 미리보기·준비 실패 안내·생명주기·태블릿 회전. 레벨 수 45 → 58(저장값 이관 없음). 소리는 재생하지 않는다. - **검증(2026-10-23, 이 맥에서 Chromium Metal·WebKit)**: 단위 **87파일·1,533개 통과**, `npm run build`(tsc strict + noUnusedLocals) 통과. E2E 전체 889개 중 기능 프로젝트(desktop-chromium 281통과·mobile-chromium 271통과)는 `audio.spec`(아래 기존 실패)만 실패했다. desktop-webkit은 전체 실행 도중 WebKit 브라우저가 시스템 부하(load average 80대)로 `Abort trap`을 내며 recut 이후 87개가 환경 실패해, 부하가 가라앉은 뒤 해당 파일(recut·scalpel·slow-food·slow·spin·split·stamp·swipe·transition·tablet)만 다시 돌려 **144통과·11건 건너뜀·실패 0**을 확인했다(앞부분 파일은 전체 실행에서 통과). perf 프로젝트는 `audio.spec` 실패 때문에 의존 프로젝트 실패로 전체 실행에서는 건너뛰었으므로 `--project=perf-desktop-chromium --no-deps`·`perf-mobile-chromium --no-deps`를 따로 돌려 **각 17개 통과**했다. 전체를 한 번에 초록으로 돌린 것은 아니다(audio 때문에 불가능) - **성능(`e2e/perf.spec.ts`, 새 단언)**: 데스크톱 52 prep 6.0 / stroke 1.7ms, 54 5.9 / 1.8ms, 58 5.9 / 1.7ms, 45 → 46 `loadMs` 3.2ms, 베는 동안 `meshBuilds` 증가 0. 모바일(기록 포함 단언 통과) 52 6.0 / 2.5, 54 6.3 / 2.5, 58 6.0 / 2.6, `loadMs` 3.2ms. 예산(`prepMaxMs` ≤ 8, `strokeMaxMs` ≤ 16)은 완화하지 않았다. desktop-webkit(perf 프로젝트 아님, 참고) 54의 `prepMaxMs`는 9~12ms로 나온다(타이머가 1ms 단위라 작은 단계의 추정이 0으로 잡히고 차가운 JSC의 외곽 SDF 8행 단계가 크게 나옴. 필요하면 `OUTLINE_ROWS_PER_STEP`을 4로 줄인다). - **perf 병렬 실행 보정**: `playwright.config.ts`가 `fullyParallel: true`·`workers: 3`이라 perf 프로젝트도 한 파일 안에서 3개 테스트가 동시에 돌아 `prepMaxMs`가 1~5ms 부풀었다(58 첫 실측 8.9ms, 52·54 7.4~7.5ms가 이 때문). `e2e/perf.spec.ts`에 `test.describe.configure({ mode: 'default' })`를 넣어 파일 안에서 차례로 돌린다. 예산·단언은 그대로이고 이 보정 뒤 데스크톱·모바일 perf 17개씩 모두 통과했다. 같은 이유로 45레벨 `prepMaxMs`(아래 기존 실패)도 혼자 돌리면 7.1~8.1ms다(`--workers=1`, 8.1ms 한 번은 여전히 예산 초과). - **기존 실패(이 작업 이전 상태에서도 재현, 기록만 하고 예산·단언은 완화하지 않음)**: - `e2e/audio.spec.ts` 6개(2개 테스트 × 3 프로젝트): `Game`이 `AudioManager`에 `effects: false`를 넘겨 샘플을 읽지 않으므로 `decoded.length === 7` 대기가 시간 초과한다. 브랜치 시작 커밋 `138da97`의 별도 worktree에서 desktop-chromium 2개가 같은 위치에서 실패함을 확인했다(`effects: false`는 그 이전부터 있음). 효과음을 다시 켤 때 고친다. - 45레벨 틀 찍기 `prepMaxMs`: 위 병렬 보정 뒤에도 경계(7.1~8.1ms)에 있다. Task 7 이전 커밋에서도 같은 값이 재현됐고 이 작업의 회귀가 아니다. - **사용자 시각 검수: 사용자 담당, 하지 않았음(미완료)**. 기초 구현 + 사용자 시각 조정이 사용자의 선택이라 에이전트는 반복 캡처·취향 수정을 하지 않았고, 어떤 리뷰어도 시각을 완료로 판정하지 않았다. 확인할 화면은 `npm run dev` → `?level=46`~`?level=58`(모바일 세로는 개발자 도구 390×844)이다. 튜닝 수치는 `src/stages/SwipeTuning.ts` 한 곳과 `src/core/SwipeFx.ts`의 리본·섬광 재질이다. - 슬래시 리본 굵기·길이·색: `RIBBON_WIDTH`·`RIBBON_COLOR`·`RIBBON_SEC`·`RIBBON_FADE_SEC`, `FX_LIFT`(비행 평면 위로 띄우는 높이), `SwipeFx`의 리본 재질(가산 혼합·끝 가늘어짐) - 섬광 밝기·길이: `FLASH_WIDTH`·`FLASH_COLOR`·`FLASH_SEC`·`FLASH_LEN`, `SwipeFx`의 섬광 재질 - 조각이 벌어지는 속도·구르기: `SEPARATE_SPEED`·`SWIPE_PUSH`·`TUMBLE_DEG`(`RECUT_ASSIST`는 4조각 판 보조 이동) - 놀이 상자 크기(젤리가 화면 폭의 약 30%): `SWIPE_BOX_SHORT`, 비행 높이 `SWIPE_FLY_Y`, 그림자 범위 `SWIPE_SHADOW_PAD` - 배경(위에서 본 테이블): 현재는 기존 테이블 장면을 위에서 본 그대로다(`SceneManager.fitBox`·바닥 평면, 전용 배경 없음) - 파편 색: `JUICE_COLOR` 표와 `JUICE_DEFAULT`, `JUICE_AMOUNT` - 히트스톱 길이·흔들림 세기: `HITSTOP`(배율·초)·`SHAKE_ONE`·`SHAKE_MULTI` - 그 밖: `x2`·`x3` 문구 위치와 CSS(`.judge.multi`는 스타일이 없다), 중력 `SWIPE_GRAVITY`, 슬래시 속도 `SLASH_SPEED`(초당 짧은 변 1.0배)·판정 창 `SLASH_WINDOW_MS`·적중 여유 `HIT_MARGIN`·최소 현 길이 `MIN_CHORD_FRAC`, 각 레벨의 발사 수치·`par`(`LevelDefinitions.ts`) - 진동 체감(실기기): `HAPTIC.swipe = [8]`(앱에서는 light). 슬래시 속도 기준도 실기기에서 맞춘다. - **미룬 사소한 지적(리뷰 중 나온 것, 모두 동작 영향 없음)**: - 단위 테스트: 잘린 조각의 `tumbleOf` 직접 테스트 없음(E2E가 구르기를 간접 확인), `PrepQueue.bump` 테스트가 약함, 발사 `hover` 속도 테스트가 약함, `Collection.test`의 도감 개수가 `levelJellyTypes` 경유(T2). - 코드 정리: `TUMBLE_DEG`가 튜플 타입이 아님, `SWIPE_TWO_SLACK`의 위치, `LevelBadge`의 삼항 연쇄, `planCut`의 `_swipeId` 미사용, 발사 시각이 지났지만 아직 `spawn`되지 않은 발사가 `applyCut` 안에서 pending으로 세어짐(T5), `SwipeHit`의 리본 필터가 이벤트마다 배열을 할당함, 들어간·나온 점에 0.08 여유가 포함됨(T3), `BodyCrossing.feed`에 외접 사각형 조기 거부 없음(측정상 불필요: `strokeMaxMs` 0.9~2.6ms). - 준비: `KidPrep`가 부분 완료 뒤 취소되면 이미 만든 것을 다시 만든다(재빌드 낭비), 비용 추정이 조각 크기를 구분하지 않음, `bakeOutlineSdfSteps`의 반복자를 취소할 때 `return()`을 부르지 않음, `SwipeEngine.dispose`(옛 `SwipeMode.dispose`)의 호출처가 없음(`Game` 종료 경로가 없음). - **알려진 한계**: - 시각 검수 전이라 배경·리본·섬광·구르기·그림자는 기초 구현이다. 놀이 상자가 커 가장자리 몸체의 그림자 품질은 보지 못했다(`setShadowExtent`로 범위를 넓히면 그림자 해상도가 낮아진다). - 실패한 판의 결과 카드 동안 아직 발사되지 않은 젤리는 날아오지 않는다(발사 중지). 카드 뒤 남은 조각은 계속 떨어진다. - 헤드리스 Chromium은 첫 pointerdown이 만드는 `AudioContext`와 첫 빠른 끌기 중간에 메인 스레드가 수십~수백 ms 멈춰 슬래시 속도에 못 미칠 수 있다(`primeAudio`와 표본 간격 약 90px로 우회). 실기기는 플랫폼 시각이라 해당 없을 것으로 보지만 확인은 사용자 몫이다. - 4조각 판의 캐시 없는 조각 베기는 보통 3프레임 안에 교체된다(섬광이 가림). 오목 젤리는 쓰지 않으므로 `cut`이 아닌 결과(또는 미리 만든 자원이 없는 `cut`)는 일어나지 않지만, 일어나면 섬광·파편은 이미 냈고 그 몸체는 더 베이지 않고 구르기만 한다(메시를 동기로 만들지 않는다). - 메인 미리보기는 테이블 판처럼 동기로 만든다(메뉴로 들어갈 때 한 번). - 컨텍스트 손실 복구 뒤 `does not belong to this context` 경고는 이 파일의 E2E에서도 거른다(three 동작). ## 틀 찍기 main 통합 (2026-10-22) - 사용자의 명시적 main 머지 요청으로 `codex/stamp-timing`(`924ed65`)과 최신 main(`548d197`)을 합쳤다. Capacitor 앱 기반·저장 어댑터·도구 반투명·썰기 감도 30%·효과음 재생 끄기를 모두 유지했다. 사용자 생성 `public/assets/`·`output/` 이미지는 포함하지 않았다. - 앱 숨김 시계와 WebGL 손실 입력 조건을 타이밍 찍기와 결합했다. `document.hidden`만 보면 네이티브 앱 숨김에도 왕복이 계속된다는 새 검사의 RED를 확인한 뒤 `platform.lifecycle.hidden`과 `ContextGuard`를 함께 보도록 수정했다. 이미 확정된 탭은 숨김·컨텍스트 손실·손 떼기로 취소되거나 다시 소모되지 않는다. - **최종 검증**: 단위 **80파일·1,466개 통과**, 타입 검사·프로덕션 빌드 통과. 타이밍 찍기 54개와 앱 숨김·컨텍스트 복구 15개를 합친 **세 브라우저의 고유 통합 검사 69개 통과**(처음 60통과·경고 분류로 9실패, 해당 9개 격리 재통과). 리뷰는 통합 코드와 후속 경고 분류 모두 Critical·Important 없음이다. - Windows ANGLE의 X4122 상수 정밀도·`f_jMarch` X4000 경고는 수정 전 main의 두 복구 검사에서도 동일하게 재현됐다. `e2e/consoleDiagnostics.ts`는 오직 `console.warn`의 알려진 전체 문구 묶음만 구분한다. 실제 컴파일 오류·새 경고·다른 미초기화 변수·오류가 섞인 로그는 남기는 단위 검사 9개를 추가했다. 복구 화소·입력·점수 단언과 성능·메시 기준은 유지했다. 제품 셰이더는 바꾸지 않았다. - 아래의 브랜치 단계 전체 E2E·성능 미해결 기록은 그대로 남는다. 이번 통합 검증을 전체 E2E·성능 통과로 해석하지 않는다. 사용자 시각 검수와 실기기 확인도 새로 완료 처리하지 않았다. - 배포 성공 여부는 이 main 커밋의 GitHub Actions `Deploy` 결과로 확인한다. ## 틀 찍기 타이밍 개편 (2026-10-21) - 스펙: `docs/superpowers/specs/2026-10-21-stamp-timing-design.md`, 계획: `docs/superpowers/plans/2026-10-21-stamp-timing.md`. - 40~45레벨은 고정 틀 아래로 젤리와 점선·찍힌 조각이 ±0.70 범위에서 초당 0.45로 좌우 왕복한다. 캔버스를 누르는 즉시 멈춰 그 위치로 찍고, 틀이 올라오고 다음 줄·모양 준비(0.20초)가 끝나면 같은 위치·방향으로 재개한다. 손 떨림·드래그·손 떼기 판정과 80px 보정을 제거했다. 자리 배치·순서·젤리·틀·IoU·별·저장값은 유지한다. - `StampMotion`은 three 없는 순수 시계이고, 좌표 역변환만 기존 세션에 넘긴다. 공통 부모의 이동은 홈과 새 레벨에서 복구한다. 새 관찰 API `stampMotion()`과 고정 대기 틀을 반환하는 `cutterScreen()`을 제공한다. - 검증: 관련 기준 테스트 369개, 새 순수 왕복 10개 RED→GREEN, 고정 틀·안내·배지 RED→GREEN. 10월 22일 전체 단위 72파일 **1,411개 재통과**, 타입 검사·Vite 프로덕션 빌드 재통과. 타이밍 찍기 E2E는 **세 기능 브라우저 × 18개 = 54개 통과**했다. stamp·progress·perception·jellies 네 파일은 최종 코드의 고유 검사 **150개 전부 통과**를 여러 실행의 로그와 현재 테스트 목록을 대조해 확인했다. 메시 준비 중 품질 교체와 실제 조각 위치 검사도 포함한다. - 전체 검증은 미완료다. 전체 실행·격리 재실행·최종 WebKit 찍기 실행을 합쳐 고유 기능 검사 575개의 통과를 확인했다. Windows WebKit 오디오 3개(샘플 디코딩 2·잠금 해제 1)와 slow-food 9·10의 윗면 여백 실패는 수정 전 `f0225aa` 소스 아카이브에서도 같은 위치·값으로 재현됐다. WebKit slow-knife 손잡이 여백과 slow의 뭉개짐·혼합 입력 시간 초과는 기준 코드 비교 없이 미해결로 남긴다. 전체 재실행은 이 실패들 뒤 중단했으므로 나머지 미실행 항목이 있고, 전체 E2E 통과를 주장하지 않는다. 데스크톱 split 3개의 이전 trace 파일 충돌은 격리 재실행에서 모두 통과했다. - 성능은 다른 자체 브라우저 검사와 겹치지 않게 데스크톱 → 모바일 순서로 실행했다. 데스크톱 7통과·6실패(21·25·26·28·45 및 21→22 전환), 모바일 10통과·3실패(21·22·45)다. 찍기 45의 준비 최대는 첫 실행 데스크톱 8.2ms·모바일 10.1ms, 격리 재측정 8.9·8.1ms로 8ms 예산을 넘었다. 같은 Windows GPU 설정의 수정 전 코드에서는 7.5·9.3ms(모바일 실패)였다. 기존 모바일 경계 초과도 확인됐으나 데스크톱 회귀 여부는 미확정이며, 어느 쪽도 최종 통과로 표시하지 않는다. 다른 성능 실패는 기준 코드 비교·원인 분석 없이 남겼다. 예산과 단언은 완화하지 않았다. - 최종 독립 리뷰(gpt-6-astra / high): Critical 0, 확인된 런타임 결함 0. Important 1은 메시 준비 중 등급 교체·실제 조각 위치 회귀 검사로 반영했다. 다음 틀 준비 0.20초 구간만 따로 누르는 전용 E2E는 Minor로 미뤘다(입력은 `stampReady`로 막음). - 이 Windows 환경에서는 Vitest의 기본 forks 대신 `--pool=threads --maxWorkers=4`로 전체 단위를 실행했다. Chromium은 Windows D3D11·macOS Metal·그 외 GL을 사용하도록 Playwright 설정을 구분했다. 임계값은 바꾸지 않았다. - **사용자 시각 검수 미완료**: 사용자 선택은 기초 구현 + 직접 시각 조정이다. `?level=40`에서 왕복 속도·틀 크기·탭 손맛을, `?level=43`~`45`에서 다음 줄·모양 준비를 확인한다. 데스크톱 1280×800·모바일 390×844의 양 끝 여백·찍기·다음 자리·결과 화면을 사용자가 확인한다. 반복 캡처·취향 조정은 에이전트가 수행하지 않았다. - 초기 업로드는 `origin/codex/stamp-timing`에 했고, 이후 사용자 요청에 따른 최신 main 통합 결과는 위 절에 기록했다. 아래 검증 수치는 통합 전 브랜치의 기록이다. 레벨 번호: G에서 기존 레벨 5~39가 11~45로 밀렸다(G 스펙 3.2절). 아래 G 이전 절들의 레벨 번호는 당시 번호다. Jelly Slice(이전 이름 Jelly Cutting)를 정밀 퍼즐에서 **큰 목적 없이 반복해서 자르는 하이퍼캐주얼 게임**으로 바꾸는 중이다(참고작: I Peel Good). 출시 대상은 Google Play·App Store지만, 일정 단계까지는 웹 배포(`jelly-cutting.steadyloop.dev`)로 확인한다. 하위 프로젝트마다 스펙 → 계획 → 구현 → 최종 리뷰 → main 머지·배포 순서로 진행한다. ## 앱 전환 1: Capacitor 앱 기반 (2026-10-20, main 머지, Android 실기기 확인 완료, iPhone 실기기·소리 남음) - 스펙 `docs/superpowers/specs/2026-10-20-capacitor-app-shell-design.md`, 계획 `docs/superpowers/plans/2026-10-20-capacitor-app-shell.md`. 사용자 선택: 기초 구현 + 사용자 시각 조정. 구조·명령·디버그 함수는 `AGENTS.md`의 '플랫폼 어댑터 (앱 기반)'·명령 절. - **구현**: Capacitor 8(core·cli·ios·android 8.5.3)로 iOS·Android 프로젝트를 만들었다(번들 ID `game.steadyloop.jelly`(2026-10-22 `game.steadyloop.jellyslice`에서 변경), 앱 이름 Jelly Slice, 번들된 `dist/`만 사용). 웹과 앱의 차이는 `src/platform/` 어댑터 한 곳이다: 저장 캐시(`KeyValueStore` ↔ Preferences), 생명주기 합치기(`Lifecycle`: 문서 + 앱 상태 + 디버그), 진동 → 햅틱 매핑(`hapticMap`), 네이티브만 화소 예산·기기 정보 시작 등급, Android 뒤로 가기(`BackButton`), 스플래시 첫 프레임 뒤 숨김, 상태 표시줄 숨김, iOS 오디오 세션 `.ambient`, Android 폰 세로 고정(`smallestScreenWidthDp < 600`)·태블릿 회전. 임시 아이콘·스플래시(홈 화면 수박 젤리, 바탕 `#efe6da`)를 `assets/`에서 `capacitor-assets`로 생성했다. 웹 동작·번들 진입 청크는 그대로다(플러그인은 동적 import). - **WebGL 컨텍스트 손실 복구**(웹·앱 공통, `ContextGuard`): 손실 중 입력·그리기·시계를 멈추고 복구되면 이어 가며, 3초 안에 복구되지 않으면 `sessionStorage` 표시(`jelly-cutting:resume`)를 남기고 다시 불러 하던 레벨의 처음으로 간다(복구 안 됨 전환 때 주소의 `?level=`을 지움). **다시 만드는 자원은 미리 준비한 다음 판 하나뿐**이고(결과 카드면 다시 준비), 조각·외곽·구멍 SDF 텍스처·`SlowStage` 맵과 투과 렌더 타깃·그림자 맵은 CPU 배열 + `needsUpdate`나 매 프레임 다시 그리기라 three가 되살린다. `e2e/context-loss.spec.ts` 15경우(가이드·반 가르기·회전판·다시 자르기·틀 찍기 두 상태·천천히 썰기 low/medium·전환·결과 카드·메뉴·복구 안 됨 3경우) × 3 프로젝트가 모두 통과해 다시 만들어야 하는 자원은 더 나오지 않았다. 복구 뒤 콘솔에 옛 GL 물체 dispose 경고가 난다(three 동작, 영향 없음). - **긴 세션 누수 검사**(Task 7): 1~45레벨 3바퀴를 돌며 레벨별 렌더러 자원 수(geometries/textures/programs)를 쟀다. **누수는 없었고 제품 코드 수정도 없다.** 2·3바퀴째가 레벨마다 완전히 같고(바퀴 끝 17/43/20) 1바퀴째는 나중 모드(회전판·틀 찍기·천천히 썰기 장면) 자원이 아직 안 올라가 시작값이 낮다(8/6/10). 텍스처는 1바퀴째에 종류별 외곽 SDF 캐시로 레벨마다 늘다가 2바퀴째부터 42~43에 고정된다. 그래서 단언은 "2바퀴째 끝 ≤ 1바퀴째 끝, 3바퀴째 레벨별 값 ≤ 2바퀴째"다(`e2e/memory.spec.ts`). - **성능 측정**(헤드리스 GPU, 전환·절단 예산 단언은 기존 그대로 통과): Chromium 21 → 22 전환 데스크톱 `loadMs` 0.8·`dropMaxGapMs` 18.3, 모바일 1.5·20.9, 4 → 5 전환 1.5·20.1, 레벨 10 다섯 칼질 `prepMaxMs` 1.1·`strokeMaxMs` 1.1(교체 순간 메시 생성 0회), 모바일 45레벨 찍기 5.2·4.4. WebKit(desktop-webkit, low, 타이머가 정수 ms): 레벨 21 진입 `loadMs` 35·곡선 `prepMaxMs` 3·`strokeMaxMs` 8, 22 진입 40·정답 직선 2·5, 10 진입 40·다섯 칼질 3·2, 45 진입 30·네 번 찍기 13·7, **21 → 22 전환 `loadMs` 3·`dropMaxGapMs` 46**(Chromium 18.3보다 큼. WebKit 타이머 정수·GPU 시간이 섞여 있을 수 있어 iOS에서 다시 확인할 항목). - **테스트**: 단위 **1462개**(77 파일, 시작 1427개), `tsc`·빌드 통과. 새 E2E: `e2e/app-lifecycle.spec.ts`(숨김·복귀·뒤로 가기), `e2e/context-loss.spec.ts`, `e2e/tablet.spec.ts`(820×1180·1180×820·800×1280, 회전), `e2e/memory.spec.ts`. 최종 검증(2026-10-20): 단위 1462 통과, 빌드 통과, E2E를 프로젝트별로 나눠 실행해 desktop-chromium 234 통과·2 건너뜀, mobile-chromium 225·11, desktop-webkit 224·12(건너뜀은 프로젝트별 기존 skip과 전용 spec), perf-desktop-chromium 13, perf-mobile-chromium 13 통과, 실패 0. `npm run app:sync` 성공(Android 포함 동기화). iOS는 `xcodebuild`(시뮬레이터, 서명 없음) 빌드·설치·실행, Android는 JDK 21(`/Applications/Android Studio.app/Contents/jbr/Contents/Home`)로 `assembleDebug` 성공. - **확인 상태(기능 사실만)**: iPhone 17 Pro·iPad Pro 11 시뮬레이터에서 설치·실행되고 홈 화면이 비지 않으며 `com.getcapacitor` 로그에 JS 오류가 없다. Android는 사용자 규칙에 따라 실기기로만 확인한다(규칙 전에 만든 AVD 2개는 지웠다). - **Android 실기기 확인(2026-10-20, Galaxy S22 Ultra SM-S908N, Android 16·SDK 36, Adreno 730, WebView 155, CSS 384×823·DPR 2.81)**: `adb` 실제 터치 + CDP(`adb forward tcp:9222 localabstract:webview_devtools_remote_` → Playwright `connectOverCDP`)로 `__jellyDebug`를 읽었다. 설치·스플래시 → 홈·상태 표시줄 숨김 정상, 시작 등급 medium(그리기 버퍼 576×1234, high는 768×1646), 하단 안전 영역 48px. 실제 터치로 레벨 1 ★★★(100%)·천천히 썰기 5 ★★★(100·100·100)·틀 찍기 40 ★★★(IoU 99.7). 대기 중 무거운 레벨(10·21·22·25·28·32·37·45) medium·high 모두 **120fps 고정**(33ms 넘는 프레임 0), 앱을 새로 시작한 상태의 찍기·썰기 연출 최대 42ms(결과 카드 등장), 1 → 2 전환 `dropMaxGapMs` 16ms, `loadMs`는 메뉴·디버그 직접 진입(미리 만들기 없음)에서 60~105ms. 백그라운드 → 오디오 suspended, 복귀 → running·판 유지(**이 WebView는 백그라운드에서 `document.hidden`이 바뀌지 않아** `appStateChange` 소스가 실제로 필요했다). 하드웨어 뒤로 가기(패널만 닫기 → 플레이 중 메인 → 메인에서 최소화), 컨텍스트 손실 → 입력 막힘 → 복구 뒤 같은 판·화면 정상, 강제 종료 뒤 진행 유지, 가로 강제에도 폰 세로 유지. logcat의 `Error injecting safe area CSS`는 실기기에서도 시작 때 나지만 안전 영역 값은 정상으로 들어온다(Capacitor 내부, 영향 없음). 사용자 확인: **진동·화면은 괜찮음, 소리는 마음에 들지 않아 나중에 따로 잡는다.** Android 태블릿은 기기가 없어 미확인. - 관찰: 품질 등급을 강제로 high → medium으로 바꾼 직후에만 셰이더 재컴파일로 75~208ms 멈춤이 났다(새로 시작한 일반 플레이에서는 재현 안 됨, 저사양에서 `FpsGovernor`가 등급을 내릴 때 같은 멈춤이 날 수 있는 기존 동작). 이 기기는 high에서도 여유가 있어, 여러 기기 자료가 모이면 고성능 기기의 시작 등급을 high로 올리는 것을 검토한다. ### 남은 확인 - **사용자 시각 확인(기초 구현 선택, 에이전트는 시각 판단을 하지 않음)**: iOS 시뮬레이터(`npm run app:ios`, iPhone·iPad 선택)에서 노치·다이내믹 아일랜드·홈 바와 HUD 겹침, 스플래시 → 첫 화면 깜박임, 태블릿 가로·iPad 창 크기 변경(iPadOS 26) 시 보기, 임시 아이콘·스플래시 외관, 시뮬레이터 홈 화면 캡처에서 보인 대각선 밝고 어두운 쐐기 모양(젤리 홈 화면의 빛 번짐인지 시뮬레이터 렌더링인지 구분 필요). Android 폰은 확인 완료, Android 태블릿 가로·세로는 미확인. - **사용자 기능 확인(iOS 시뮬레이터, 같은 명령)**: 모드별 한 판(가이드·곡선·반 가르기·회전판·다시 자르기·틀 찍기·천천히 썰기), 소리, 앱 종료 후 재실행 시 진행·설정 유지, 백그라운드 → 복귀(판 유지·소리 재개), 폰 세로 고정·iPad 회전. - **소리(사용자 판단: 마음에 들지 않음)**: 앱에서 들은 절단음·썰기 소리를 다시 잡는 별도 작업으로 한다. - **iPhone 실기기 체크리스트(미완료)**: 햅틱 체감과 매핑 값(길이 ≤ 10ms light·11~20ms medium·≥ 21ms heavy, 판정 `[20, 40, 30]`·tremor `[6]`·slice `[6]` 0.1초 간격이 거슬리지 않는지), 무거운 레벨(10·21·22~28·45)의 GPU 프레임·발열(화소 예산 값 조정 포함), 무음 스위치(`.ambient`), 실제 메모리 부족 때 컨텍스트 손실 복구, 저전력 모드, iOS WebKit 21 → 22 전환 `dropMaxGapMs` 46ms(위 측정값, 실기기·시뮬레이터에서 끊김이 보이는지). ### 알려진 한계·미룬 항목 - iOS 시뮬레이터 첫 실행은 스플래시가 길게(약 20~30초) 남은 뒤 홈이 보였다(첫 셰이더 컴파일, 시뮬레이터 GPU 때문으로 보이며 실기기 시간이 아니다). 실기기에서 첫 실행 시간을 확인한다. - 스플래시는 `Game` 첫 프레임 뒤에 숨기고, 시작이 예외를 내면 `main.ts`가 바로 숨기며, 네이티브는 8초(`SPLASH_SAFETY_TIMEOUT_MS`) 안전 타이머가 있다(최종 리뷰 지적 반영). Android 폰 세로 고정은 `onConfigurationChanged`에서도 다시 판별한다(접이식 펼침). - 네이티브 햅틱은 `setTimeout`으로 예약하므로 진동을 끈 직후 예약된 한 번(최대 60ms)이 울릴 수 있다(실기기 햅틱 조정 때 처리). iOS `Info.plist`의 `UIRequiredDeviceCapabilities` `armv7`(템플릿 기본값)은 출시 준비 때 확인한다. - `UIViewControllerBasedStatusBarAppearance`가 false라 iOS에서 `StatusBar.hide()`가 효과 없을 수 있다(`UIStatusBarHidden` true로 이미 숨겨지고 실패는 무시한다). - 컨텍스트 손실 포기 시계는 rAF 프레임에서만 흐르고 프레임당 0.1초로 제한된다(숨겨진 탭에서는 멈추고 복귀 뒤 3초를 다시 센다). 의도한 동작이다. - 앱 안 `Haptics` 호스트는 `vibrate`가 항상 있다고 응답하므로(진동 미지원 안내가 뜨지 않음) 기기별 햅틱 엔진 유무는 보지 않는다. 진동 설정 스위치는 그대로 동작한다. - `public/`·`icons/` 등 `capacitor-assets`의 PWA 부산물은 웹 불변을 위해 지웠다(아이콘 원본은 `assets/`). ## 개발자 전체 레벨 목록 (2026-10-19, main 배포) - 참여자 직접 테스트 편의를 위해 홈 History 왼쪽에 `Developer` 버튼을 추가했다. 등록된 모든 레벨을 나열하고, 각 레벨의 Play는 History Replay와 같은 경로다(별·방문·도감 기록, 진행 위치 유지, 클리어 후 홈 복귀). `MainMenu.DEV_TOOLS = true`로 개발 기간 동안 운영에서도 켜 둔다. on/off는 후속 작업이다. - 검증: 빌드 통과, `e2e/home.spec.ts` 36개(3 프로젝트) 통과. Opus / medium 독립 리뷰 Critical 없음. - 알려진 문제: 시각 검수는 사용자 담당(미완료). 폭 320px 기기에서 홈 상단 줄이 약 39px 넘칠 수 있다. `tests/JellyPiece.test.ts`의 `setCrush` 테스트가 음식 모양 젤리 변경(`uCrush` 주인이 `setSlowDeform`으로 바뀜) 뒤 옛 규칙을 검사해 CI가 실패했다. 테스트를 현재 규칙으로 고쳤다. ## 음식 모양 젤리·절단 손맛·판정 UI (2026-10-19) - 후속 설계 `docs/superpowers/specs/2026-10-19-slow-food-shape-and-cut-feel-design.md`, Native 계획 `docs/superpowers/plans/2026-10-19-slow-food-shape-and-cut-feel.md`. 사용자 선택: 음식6종 모두, 전체 시각 검수, 현재 폴더 새 브랜치, 영향 스테이지만 테스트. - **구현**: 5~10레벨 전체를 실제 캡슐/타원체 음식 젤리로 교체했다. 흰 블록·별도 단단한 속 물체를 제거하고 껍질·과육·씨를 실제 몸체/단면 좌표로 그린다. 깊이별 미리보기와 그림자 마스크, 국소 눌림·잔여 손상, 실제 원판의 3D 피벗·지지, 표면에 붙는 표시·실제 접촉점 파편을 연결했다. 칼질 수·폭0.25·채점·저장·소리/진동 규칙은 유지한다. - **판정 패널**: 짙은 불투명 바탕에 상태 제목·행동 안내를 나누고 칼질 번호·굵은 기록 막대를 표시한다. 준비 실패 안내와 Home 복귀도 제공한다. - **검증**: 영향 단위 **292개 통과**, 빌드·타입 검사·diff 공백 검사 통과. 선택 기능 **115개 통과·2개 조건부 건너뜀**, 선택 성능 **4개 통과**. 최초 기능96개 중 준비/혼합 입력 검사2개×3브라우저가 실패했으며 새 준비 게이트/표시 깊이 추종에 맞춰 검사 입력을 고친 뒤 해당6개와 추가21개를 묶은27개가 모두 통과했다. 실패·수정 후 로그를 함께 보존한다. 전체 단위/E2E는 실행하지 않았다. - **성능**: 레벨10 다섯 칼질 prepMaxMs 데스크톱1.60/모바일1.30, strokeMaxMs1.40/1.20, 같은 칼질 ready 이후 교체 메시 생성0회. 4→5 loadMs2.20/2.40, dropMaxGapMs19.70/19.50(후자는 모바일 측정값이며 데스크톱만34ms 기준 단언). 모든 성능은 기능/캡처 종료 후 데스크톱→모바일 순서로 실행했다. - **시각 증거**: `.playwright-mcp/slow-food-shape/manifest.json`의 기본 **96장**(1280×800·390×844, low/high). 음식6종 시작·첫 조각·완료72장, 오이 부분 절단12장, hard완료/jerky8장, 경계 전환4장이다. 상태/모바일 수정 재촬영과 연락시트·짧은 재개/관통/착지 영상도 같은 폴더에 있다. 콘솔/GLSL 오류0 및 GPU/CPU 위치·법선 오차1e-4 이하를 실제 브라우저에서 확인했다. 로그는 `logs/`에 있다. - **독립 리뷰**: `gpt-6.1-sol / medium` 코드 리뷰와 시각 검수 완료. Important였던 모바일 마지막 칼 손잡이 화면 밖 잘림은 실제 정점 회귀 RED→GREEN 및 low/high 원본 재검수로 해결했다. 세로 칼0.58배·대기 물러남0을 사용한다(가로 기존0.88배·−0.28). 음식20px 여백은 유지하며 칼은 전 프레임 화면 안2px 안전여백으로 확인한다. - **남은 경미한 항목**: 완료 직전 칼이 마지막 원판의 단면 중앙 일부를 가린다. 모바일 축소로 완화됐으며 절단·진행에는 영향이 없다. 완료 연출의 추가 조정은 보류했다. - **검증 한계/배포**: 모바일은 브라우저의 세로 뷰포트·터치 검증이며 실제 Android/iOS 기기는 검증하지 않았다. 이번 범위 밖 스테이지 전체 회귀는 미실행이다. 브라우저 권한 자동 검토 용량 오류로 잠시 중단됐으나 같은 모델에서 복구해 전체 시각 검수를 끝냈다. main 병합·푸시·배포는 실행하지 않았다. 아래 G 절의 블록 외형·옛 전체 테스트 수치·시각 자료는 2026-10-18 당시 이력이다. 현재 외형과 이번 검증 범위는 이 절 및 후속 설계가 우선한다. ## G 천천히 썰기 레벨 5~10 (2026-10-18, 브랜치 `feature/slow-slicing`, 최종 독립 리뷰·머지 전) - 스펙 `docs/superpowers/specs/2026-10-18-slow-slicing-levels-design.md`, 계획 `docs/superpowers/plans/2026-10-18-slow-slicing-levels.md`. - **구현 범위**: 레벨 5~10 천천히 썰기(긴 오이 3칼 → 애호박 3 → 망고 4 → 파파야 4 → 멜론 5 → 가지 5). 화면 아래로 끌어 칼을 직접 눌러 내리고, 왼쪽 끝부터 폭 0.25의 얇은 원판을 떼어 내 단면이 위로 오게 비늘처럼 쌓는다. 끝까지 썰면 항상 통과하고 별은 부드럽고(세게 밀지 않기) 고르게(툭툭 밀지 않기) 썬 정도(`Smoothness`)로 정한다. 기존 5~39는 11~45로 재배치하고 저장값을 한 번 이관한다(`jelly-cutting:levels-version` 2). 낮은 옆 시점(`SLOW_VIEW` 앙각 18°)·물방울 크림 테이블·따뜻한 역광·slow 음식만 three 투과(low 끔), 투명 젤리 블록 속 곧은 음식 몸통, 뭉개짐·파편·상태 표시·깊이 막대·결과 근거, 레벨 5 튜토리얼, 사용자가 고른 썰기 소리(고리 2·마무리 3 랜덤), 끄는 동안 0.1초마다 `[6]` 진동. 구조는 `AGENTS.md`의 '천천히 썰기 (G)' 절. - **정한 수치**(스펙에 기록): `vmax` 0.40 → 0.30, `k` 24, 감도 화면 높이 55% = 깊이 1, 급함 `a` 0.5·`b` 0.75·평활 0.06초, 조각 폭 0.25, 쌓임 65°~75°, 뭉개짐 높이 ×0.75·수평 ×1.12(썬 조각 ×0.6·×1.2), 파편 불투명 0.93·최대 160, 칼 0.88배·기울기 0.12, 대기 높이 0.22 × 두께·물러남 0.28, 역광 3.2, 투과 두께 0.35, 스프링 300/13/0.09 → 400/15.5/0.065. - **테스트**: 단위 **1369개**(64 파일, G 시작 1088개)·`tsc` 통과. `npm run build` 통과. E2E 전체(`npm run e2e`, 성능 포함) **573 통과·5 건너뜀(조건부)·실패 0**(13.1분). 성능: 레벨 10 다섯 칼질 `prepMaxMs` 데스크톱 2.6·모바일 2.4, 교체 순간 메시 생성 0회, 4 → 5 전환 `loadMs` 1.3·`dropMaxGapMs` 18.2, 45레벨 찍기 `prepMaxMs` 4.6·4.8(예산 8). - **시각 검수**(전체 시각 검수, Task 11 사용자 방향 승인 + Task 15 6종 × 상태 × 1280×800·390×844 × low·high): 결과 `.playwright-mcp/slow-slice/t15/`(캡처 스크립트 `.playwright-mcp/slow-slice/t15.cap.ts`, 연락시트 `sheets-r*`). Task 15는 r1(6종 × 상태 × 2해상도 × 2등급, 184장)과 r2(레벨 6·8·10 재캡처 72장) 두 라운드다. 고친 것: 가지 단면 씨 점이 비스듬히 눕힌 조각에서 2~3px라 안 보임 → 칸 0.044·반경 0.017, 새 조각·새 재질 첫 프레임 투과 세기 1 가능성 → 투과 세기를 그리기 직전에 정함. 망고는 주황·붉은 기·초록 끝이 보여 감자처럼 읽히지 않는다고 판단해 그대로 뒀다. 세게 밀기 상태가 레벨 6·8·10 캡처에서 `Smooth`로 찍힌 것은 캡처 입력(손가락이 화면 끝에 닿아 눌림이 풀림) 문제라 캡처를 고쳤다(제품 정상). - **미확인(사용자 확인 필요)**: - 무음·진동 꺼짐 화면만으로 "지금 세게 누르는지, 어떻게 고치는지, 어디서 깎였는지, 다음 별 조건"을 말할 수 있는지(스펙 10절 이해도 확인). - Android 실기기 진동 체감(끄는 동안 규칙적 진동, 손 뗄 때 멈춤, 다시 끌 때 재개). 사용자 지시로 실기기 없이 웹(Playwright)만 테스트했고, 브라우저 요청 로그(`hapticLog`)만 E2E로 확인했다. - 실제 사람 손으로 끄는 감도·`vmax` 손맛과 감점 경계(꾸준히 10~15% 넘게 빠르면 ★로 가파르게 떨어짐, 스펙 4.3절). - 청음: 고른 소리가 실제 게임 안에서 반복감·잡음 없이 들리는지. - **최종 독립 리뷰**(Opus 5.5 / medium): Critical·Important 없음. 지적(등급 변경 중 미리 만들기 중복 대입 누수, 문서의 옛 레벨 번호, 진동 끄기가 소리까지 끔, 테스트 제목 번호)은 최종 수정으로 반영했다. - **출시 전 확인**: 썰기 소리 5개는 제3자 YouTube 영상(영상 1) 오디오에서 뽑았다. Google Play·App Store 출시 전에 사용 권리를 확인하거나 직접 만든 소리로 바꿔야 한다(`src/audio/samples/SOURCES.md`). 기존 절단음 2개도 같은 출처 계열인지 함께 확인한다. - **알려진 한계·미룬 항목**: - 시각: 칼질 사이 대기 칼이 막 쌓인 조각의 단면 위쪽을 가린다(대기 높이·물러남을 늘리면 데스크톱 손잡이가 HUD를 덮거나 모바일에서 화면 밖으로 나가 그대로 둠). 모바일 세로에서 대기·쌓임 동안 칼 손잡이 끝이 화면 오른쪽 밖으로 잘리고 마지막 칼질 뒤 0.35초 손잡이가 화면 밖에 있다. 쌓인 조각은 서로 겹쳐 단면이 맨 위 조각만 크게 보인다(영상의 비늘 쌓임과 같은 방식, 승인된 방향). 세게 밀어 뭉개질 때 젤리 블록 아래 왼쪽 모서리에 속 몸통 색이 가는 꼬리처럼 비친다(가지 high에서 보임, 몸통 되돌림 `vSlowOff`의 모서리 보간). 애호박·오이 오른쪽 끝 윗면의 작은 턱, 홈 화면 오이 윗면 하이라이트 줄, 물방울 반짝임 약함, 세로 화면 HUD 아래 넓은 어두운 배경(영상의 어두운 위쪽 배경을 따른 것), 모바일 시작 배지 위치 `bottom: 132px` 하드코딩(지금 문구·안내와 겹치지 않음), 레벨 5 튜토리얼 손가락이 대기 칼날·음식 위를 지나감. 이해도는 사용자 확인 전이다. - 성능: perf-mobile 45레벨 틀 찍기 `prepMaxMs`는 G 이전부터 예산(8) 경계에서 흔들렸고, G가 더한 맵 굽기·파편 쓰레기로 악화된 회귀는 slow 장면 자원 지연 생성으로 되돌렸다(5.5/6.85/7.9). slow 레벨 입력 응답이 약 2프레임 느리고, `?level=5` 직접 진입 때 맵 굽기가 한 프레임에 든다. 39→40(틀 찍기 진입) 전환 중 틀 조각 미리 데우기 비용은 측정하지 않았다. - 코드(미룬 사소한 지적, 원장): 저장 이관 쓰기 중간 실패 시 재이관 +6 중복 가능(단위 테스트 없음), `submitSlowCut` 점수 범위 검증 없음, `previewSlowCut` 첫 호출이 id 순번을 씀, `verticalChordMid`가 첫 겹침 구간만 봄(오목 외곽 음식 추가 시 재검토), 바닥 통과 cancel 뒤 두 번째 손가락이 다음 칼질을 시작할 수 있음(첫 포인터만 해석), `finishSlowCut` 세대 검사 미도달 시 Promise 매달림(기존 패턴), 진동 끄기가 오디오 고리까지 멈춤, `visibilitychange` 리스너 미해제, `DepthDrag.dispose`가 `touchAction` 원복 안 함, HUD `role=status` 문구가 자주 바뀌어 스크린 리더 반복, 일부 테스트 제목의 옛 레벨 번호, 쌓임 높이·교대 패턴 단언 없음, E2E `dragBuilds` 상한 단언 없음. - slow 레벨은 실패가 없어 Retry E2E가 없다(코드는 `stopSlowFeel` 호출). ## 판 전환 끊김 개선 (2026-10-18) - 클리어 → 다음 판 전환에서 쓸려 나감이 끝나고 새 젤리가 떨어지기 시작하는 순간 한 번 끊겼다. 원인은 `loadLevel`이 한 프레임에 첫 젤리 메시·조각 SDF·외곽 SDF(256², 종류당 처음 한 번)를 만들고(40~67ms), 첫 렌더에서 새 종류의 셰이더를 컴파일하던 것(15~40ms)이다. - 결과 카드가 떠 있는 동안 `Game.prepareLevel`이 다음 판의 첫 젤리를 프레임을 나눠 만들고 셰이더를 미리 컴파일·그려 둔다. 실측(헤드리스 Chromium GPU): `loadLevel` 45~67ms → 2~6ms, 떨어짐 첫 프레임의 긴 프레임(50~67ms) 사라짐. - 회귀 기준: `e2e/perf.spec.ts`의 15 → 16 전환 `loadMs` ≤ 16(수정 전 41~48ms로 실패 확인)과 데스크톱 `dropMaxGapMs` ≤ 34(정상 18~21ms, 전체 E2E 부하에서 28ms, 셰이더 예열을 빼면 39~51ms). 모바일 뷰포트는 프라이팬 셰이더 GPU 비용만으로 33~36ms라(예열 없음 42~46ms) 기록만 한다. 단위 `tests/OutlineSdf.test.ts`(행 나눠 굽기 = 한 번에 굽기)·`tests/JellyMaterial.test.ts`(나눠 구운 캐시를 재질이 씀). - 최종 독립 리뷰(Opus 5.5, medium): 머지 가능, Critical 없음. Important(perf가 셰이더 컴파일 끊김을 못 지킴)는 `dropMaxGapMs` 단언으로 반영했고, Minor 중 등급 변경 시 다시 준비·마지막 레벨 실패 Retry 준비·준비 예외 처리·캐시 테스트 강화를 반영했다. 미룬 것: 준비 단계별 프레임 비용 측정 없음, 준비 수명(Retry 직후·준비 도중 Home·warm 메시 잔존)을 직접 검사하는 테스트 없음. - 39레벨 찍기 `prepMaxMs` 성능 테스트는 수정 전 main에서도 6.4~8.3ms로 예산(8) 경계에서 흔들린다(아래 F1 알려진 한계, 이번 변경과 무관). ## 절단음만 남기기 (2026-10-17) - 사용자 요청으로 식칼·메스의 선택 ASMR 두 샘플만 남겼다. 압축·출렁임·판정 차임·성공/실패·넘어짐·쓸림·착지 소리의 Game 호출을 제거했다. 버튼 클릭음과 판정·콤보·진동·물리 연출은 유지한다. - `e2e/audio.spec.ts`는 실제 BufferSource 재생 전체와 Oscillator 시작을 관찰하여, 절단부터 자동 다음 판까지 선택 샘플 두 개 외의 소리가 재생되지 않음을 검증한다. 기존 빌드의 두 테스트가 합성 잡음 때문에 실패함을 먼저 확인했다. - 빌드·단위 868개·세 브라우저의 audio·feel·transition E2E 66개 통과. `gpt-6.1-sol / medium` 최종 독립 리뷰도 지적 없이 승인했다. 로그 `/tmp/jelly-slice-only-{build,unit,e2e}.log`. 계획 `docs/superpowers/plans/2026-10-17-slice-only-audio.md`, 사운드 규칙은 손맛 스펙 5절을 갱신했다. 이번 오디오 변경은 아직 커밋·머지·푸시하지 않았다. ## Jelly Slice 홈 개편 (2026-10-17, 검증·최종 독립 리뷰 완료·main 반영) - 승인 명세: `docs/superpowers/specs/2026-10-17-jelly-slice-home-design.md`, 계획: `docs/superpowers/plans/2026-10-17-jelly-slice-home.md`. - 표시명을 Jelly Slice로 변경했다. 홈에 입체 타이포, 현재 레벨의 실제 3D 젤리, Level·Play, 좌측 누적 별과 우측 히스토리·도감·설정 SVG 아이콘을 둔다. 홈에서는 가이드·칼·게임 HUD를 숨기고 Play 뒤에 복원한다. - 별은 레벨별 최고값의 합이며 개선 차액만 늘어난다. 방문한 레벨은 실패도 0별로 히스토리에 남고 재플레이가 원래 진행 위치를 바꾸지 않는다. 재플레이 Retry는 같은 레벨을 유지하고 클리어 뒤에는 홈으로 돌아온다. 전체 완료 후 홈은 마지막 레벨을 보여준다. - 도감은 실제 분할(`consumed`) 순간 종류별 한 번 등록한다. `jelly-cutting:collection`·`jelly-cutting:visits`를 추가하고 구버전 별 기록에서 확인 가능한 항목만 이행한다. 소리·진동은 설정 패널의 두 스위치에서 조작하며 기존 키와 HUD 소리 상태를 공유한다. 기존 ASMR 작업은 보존했다. - 홈 젤리는 스프링으로 2.4초마다 출렁인다. 패널·숨긴 탭·동작 줄이기에서는 반복 충격을 멈춘다. `guidesVisible()`·`menuWobble()` 읽기 전용 디버그를 추가했다. - 검증: 최종 빌드 통과, 단위 868개 통과. E2E는 세 기능 브라우저·성능 두 프로젝트의 고유 테스트 444개 통과(기존 조건부 제외 3개). 최초 전체 실행은 420통과·결과 폴더 충돌에 따른 trace 수집 실패 3개·기존 제외 3개·성능 의존 대기 18개였다. 충돌 3개를 격리 재실행해 통과했고, 수정된 홈 30개(새 키보드 테스트 3개 포함)와 성능 18개를 별도 실행해 모두 통과했다. `/tmp/jelly-slice-{unit,e2e,home-final,spin-final,perf-desktop,perf-mobile}.log`. - 시각 검수: 390×844·360×640·1440×900 캡처 33장에 콘솔 오류·홈 가이드 노출·레이아웃 겹침 없음. 실제 변형·중지 조건·가로 방향·긴 목록 끝 스크롤 추가 검증 통과. 접근성 수정 후 실제 본문 포커스·PageDown도 재검증·캡처했다. 산출물 `.playwright-mcp/jelly-slice-home/`. - 독립 명세 리뷰 통과. 품질 리뷰의 도감 키보드 스크롤 P2는 실패 재현 후 본문 `tabIndex=0`으로 수정하고 코드 재검토·세 브라우저 회귀를 통과했다. 구현·명세·품질·최종 독립 리뷰는 모두 `gpt-6.1-sol / medium`으로 수행했다. 최종 리뷰는 소스 18개 파일의 SHA, 실제 로그·연락시트·최신 키보드 캡처를 확인하고 Critical·Important·Minor 지적 없이 승인했다. - 실기기 진동 체감은 이번 브라우저 검증에 포함하지 않는다. 도구 상점·별 소비는 후속 기획이며 이번에는 구현하지 않았다. 머지·푸시·배포 전이다. ## 로드맵 | # | 하위 프로젝트 | 상태 | 스펙 · 계획 | |---|---|---|---| | A | 레벨 진행 골격: 레벨 1~22, 별 판정, 자동 진행, 레벨이 정하는 칼, 칼 인지 장치 | **완료** (2026-10-14 배포) | `docs/superpowers/specs/2026-10-14-level-progression-design.md` · `docs/superpowers/plans/2026-10-14-level-progression.md` | | B | 손맛 연출: 칼질 판정(Perfect/Great/Good), 콤보, 슬로모션·흔들림, 진동, 사운드 확장, 넘어짐 → 쓸려 나감 → 떨어짐 전환 | **완료** (2026-10-15 배포) | `docs/superpowers/specs/2026-10-15-game-feel-design.md` · `docs/superpowers/plans/2026-10-15-game-feel.md` | | C1 | 과일·채소 젤리 13종, 1~15레벨 모두 다른 젤리, 단면 부품 라이브러리 | **완료** (2026-10-16 배포) | `docs/superpowers/specs/2026-10-15-fruit-veg-jellies-design.md` · `docs/superpowers/plans/2026-10-15-fruit-veg-jellies.md` | | C2 | 주방·공구 사물 3종(프라이팬·망치·열쇠), 16~22레벨 모두 다른 사물, 주축·눈대중 직선 함정, 15·반 가르기 성능 | **완료** (2026-10-16 배포) | `docs/superpowers/specs/2026-10-15-kitchen-tool-gadgets-design.md` · `docs/superpowers/plans/2026-10-15-kitchen-tool-gadgets.md` | | C3 | 사물 단면 속 구조: 반 가르기 사물 7종을 자르면 그럴듯한 진짜 속(부품·뚫린 구멍·재료색 젤리와 결)이 보임, 넘어짐 각도 16°~30° | **완료** (2026-10-16 배포) | `docs/superpowers/specs/2026-10-16-cutaway-interiors-design.md` · `docs/superpowers/plans/2026-10-16-cutaway-interiors.md` | | D1 | 회전판 23~28: 젤리가 가이드와 함께 판 위에서 돌고, 손을 떼는 순간 판이 멈춰 그 각도로 잘림. 속도 30→55°/s, 26부터 방향 반전·완급 | **완료** (2026-10-16 배포) | `docs/superpowers/specs/2026-10-16-new-gimmicks-design.md` · `docs/superpowers/plans/2026-10-16-d1-spinning-plate.md` | | D2 | 조각 다시 자르기 29~33: 자를 때마다 큰 조각 위에 반 가르는 새 가이드, 카메라 포커스·나머지 조각 흐림 | **구현 완료** (브랜치 `feat/d2-recut`, 최종 리뷰·머지 전) | 같은 스펙 3절 · `docs/superpowers/plans/2026-10-16-d2-recut.md` | | F1 | 틀 찍기 34~39: 쿠키 틀(별·하트·달·꽃)을 누르고 끌어 자리에 맞춰 떼면 찍힘, 손 떨림·떨림 파동 진동, 점수 = IoU, 평평한 새 사물 6종(컵받침·비스킷·스마트폰·코믹북·쟁반·백과사전) | **완료** (2026-10-18 배포) | `docs/superpowers/specs/2026-10-17-cookie-cutter-stamp-design.md` · `docs/superpowers/plans/2026-10-17-f1-cookie-cutter-stamp.md` | | G | 천천히 썰기 5~10: 칼을 직접 눌러 내려 음식 젤리 6종을 얇은 원판으로 여러 번 썰기, 낮은 옆 시점·투과·쌓임, 부드럽게·고르게 판정, 썰기 소리·진동, 번호 재배치(5~39 → 11~45)·저장값 이관 | **구현 완료** (브랜치 `feature/slow-slicing`, 최종 리뷰·머지 전) | `docs/superpowers/specs/2026-10-18-slow-slicing-levels-design.md` · `docs/superpowers/plans/2026-10-18-slow-slicing-levels.md` | | F2 | 단면 속 구조: 새 사물 6종을 `CUTAWAY_TYPES`에 더하고 틀 외곽 ★ 검사로 속 구조를 그림 | **다음** | 같은 스펙 4.4절 | | H | 휘둘러 베기 46~58: 위에서 내려다본 놀이 상자 안을 날아가는 젤리를 손가락으로 빠르게 그어 베기, 12방향 넓이 중심 절단·미리 만든 결과 바꿔치기, 4조각 판, 휘두른 횟수 별, 히트스톱·진동·리본·섬광 | **main 머지** (`f0d5a47`, 시각 검수 사용자 담당·미완료) | `docs/superpowers/specs/2026-10-22-swipe-slicing-design.md` · `docs/superpowers/plans/2026-10-22-swipe-slicing.md` | | 앱 1 | 앱 전환 1: Capacitor 앱 기반(iOS·Android 프로젝트, 플랫폼 어댑터, 햅틱 매핑, 컨텍스트 손실 복구, 태블릿·회전, 임시 아이콘) | **완료** (main 머지, Android 실기기 확인, iPhone 실기기·소리 남음) | `docs/superpowers/specs/2026-10-20-capacitor-app-shell-design.md` · `docs/superpowers/plans/2026-10-20-capacitor-app-shell.md` | | 앱 1.5 | `Game.ts` 리팩토링(동작 변화 0, 단독 브랜치) | **다음** | — | | 앱 2 | 수익화 연동(광고·인앱 결제 등) | 대기 | — | 최종 레벨 구성은 레벨 진행 스펙 1.2절에 있다. ## 선택 ASMR 절단음 (2026-10-17) - 짧은 후보 중 사용자가 선택한 **2번 딸기(0.42초)·5번 오이(0.31초)**만 식칼·메스 절단음으로 사용한다. 선택본과 같은 WAV를 `src/audio/samples/`에 두고 번갈아 원래 속도·음높이로 재생한다. 출처·구간·해시는 `SOURCES.md`에 있다. - 기존 합성 절단음·껍질 클릭·물컹한 톤을 교체했다. 두 샘플은 첫 사용자 입력 때 한 번 읽고 디코딩하며, 로딩 전·실패 시 절단음만 생략한다. 다른 효과음·음소거·탭 숨김 처리는 유지한다. - 검증: 단위 전체 **864개**, 타입 검사·프로덕션 빌드 통과. 오디오 E2E **6개**(데스크톱 Chromium·모바일 Chromium·WebKit × 식칼·메스)에서 실제 디코딩·두 샘플 교대·재생 속도 1 확인. 시각·입력 변경은 없으며 전체 E2E·실기기 청음은 이번 작업에서 수행하지 않았다. - 손맛 스펙 5.0절·`docs/superpowers/plans/2026-10-17-asmr-cut-sounds.md`. 머지·푸시·배포 전이다. ## 현재 상태 (F1 구현 시점) - 레벨이 39개다. 34~39는 틀 찍기: 34 컵받침(별 1, 큰 `Stamp it` 배지·튜토리얼), 35 비스킷(하트 1), 36 스마트폰(달 2), 37 코믹북(별 3), 38 쟁반(하트 3), 39 백과사전(별 → 하트 → 달 → 꽃 4). 화면을 누르면 자리의 모양·기울기 그대로인 틀이 손가락(터치는 80px 위)을 따라다니고, 떨림이 더해진다. 떼면 그 위치에 찍힌다(젤리에 닿지 않으면 횟수를 쓰지 않음). 점수 = 100 × IoU, `minAccuracy` 45 → 58. 마지막 찍기 뒤 나머지 젤리가 쓸려 나가고 틀 조각이 넘어져 단면을 보인다. - 나머지 젤리는 조각 다각형이 아니라 원본 메시를 구멍 SDF로 `discard`한다(`holes` 재질). 다른 젤리 셰이더의 컴파일 원문은 그대로다(해시 스냅샷 기존 19줄 유지, 새 6종 추가). - 테스트: 단위 **1082개**(55 파일, 시작 868개)·`tsc`·빌드 통과. 틀 조각 메시 퍼즈(`tests/StampMesh.test.ts` + `stampFlat` 묶음 `JellyMeshFuzz.stampFlat.test.ts`, 임계값 그대로). E2E: 전체 실행(Task 11) 484 통과·원래 건너뛰는 3개·실패 1(perf-mobile 39레벨 `strokeMaxMs` 16.6ms) → 찍기 판정 빠른 경로로 해소해 성능 두 프로젝트 3회 연속 20개 통과, 기능 3개 프로젝트의 stamp·progress·jellies·perception 114개 통과. 시각 검수 뒤 수정은 stamp·jellies 42개, 활성 자리 점선 대비 수정은 stamp·jellies·gameplay desktop-chromium 35개 통과. 최종 리뷰 전 전체 `npm run e2e`: 464 통과·3 건너뜀·1 실패(웹킷 도감 키보드 스크롤, 단독 통과 → 부하 흔들림), 성능 두 프로젝트 단독 20개 통과(39레벨 `prepMaxMs` 7.4). 최종 리뷰 Minor 수정 뒤 단위 1085개, stamp·home·progress·split·gameplay E2E(세 프로젝트, 동시 실행 1~2: 호스트 부하가 매우 높아 3 이상에서는 수정 전 HEAD도 환경 실패) 통과. - 성능(39레벨 네 번 찍기, 빠른 경로 뒤 3회): 데스크톱 `prepMaxMs` 6.8~6.9·`strokeMaxMs` 4.5~5.2, 모바일 7.6~8.0·5.1~5.6(예산 8·16). 구멍 SDF 굽기를 행 8개씩 프레임을 나눈 뒤 값이다(나누기 전 5~11ms). - 시각 검수: 전체 시각 검수(데스크톱 1280×800·모바일 390×844 × low·high × 8상태 + 보조 캡처: 시작 6종, 집어 들기 직후·들고 있기, 39 누름·끝, 37·38 구멍 가장자리·두 번째 자리, 쓸림, 넘어짐 끝, 34 튜토리얼 빗나감, 가장자리 찍기). 1회차에서 착지 그림자 기울기, 누름 중 점선 사라짐, 쓸린 뒤 점선 잔존, 틀 내림 지연, 튜토리얼 힌트 잔존, 쟁반 줄무늬·백과사전 `step` 경계를 고쳤고, 리뷰 지적(활성 자리 점선 대비)을 반영했다. 캡처 도구·결과: `.playwright-mcp/f1/`(`capture.{config,spec}.ts`: `ROUND`·`ONLY`·`QUALITIES`, `sheet.mjs`, `crop.mjs`, 최종 연락시트 `sheet-final-{desktop,mobile}-{high,low}.png`, 전후 `sheet-before-after.png`·`sheet-fix1-*`). ### F1에서 사용자 대신 내린 판단 - 집어 드는 흔들림 `pickup`을 계획의 0.12에서 0.25로 키웠다(0.12로는 누르자마자 뗀 찍기가 34·35·36·38·39에서 `minAccuracy`를 넘었다). 체감은 시각 검수에서 확인했다. - 38레벨 쟁반은 하트 세 개가 한 줄에 들어가지 않아 위 둘·아래 하나 삼각 배치다. 달은 두 원의 차집합 통통한 초승달(크기 ≥ 0.4)이다. - 코믹북·백과사전 세로 반폭 0.8749(기존 수박 외접 사각형 테스트 기준 유지), 틀 모양 구분 테스트는 넓이 차이가 아니라 윤곽 IoU < 0.85로 한다(하트·꽃 넓이가 거의 같음). 떨림 파동 포락선 정규화 상수(약 1.744), 넘어짐 축 (−1, 0)·24°, `JellyThumbnail`의 새 종류 이름·색(Coaster·Biscuit·Phone·Comic Book·Tray·Encyclopedia)을 구현이 정했다. - 찍기 판정에 빠른 경로를 넣고(성능 예산 완화 없음) 구멍 SDF 굽기를 프레임으로 나눴다. ### F1 알려진 한계·후속 - 구멍 안쪽 벽이 없다. 나머지 젤리는 구멍을 `discard`로만 그려 조각 베벨과 구멍 경계 사이로 구멍 먼 쪽에 3~4px 틈이 보인다(깜빡임·Z 싸움은 없고 절단 홈처럼 읽힘). 안쪽 벽 메시를 더하는 일은 범위가 커 후속이다. - 모바일 `prepMaxMs`가 7.6~8.0으로 예산(8)에 근접하다(조각 메시 프리빌드의 최대 한 덩어리). 부하가 큰 머신에서 흔들릴 수 있다. - 결과 카드가 페이드인할 때 쉬는 틀이 카드 아래쪽 위로 잠깐 겹친다(기존 칼 퇴장과 같은 경로). - 최종 독립 리뷰(Opus 5.5, medium): 머지 가능, Critical·Important 없음. Minor 4건 수정: `presentStamp` 준비 예외 시 멈춤 방어(try/finally), `tremorPulseStarts` NaN·[0, 0] 가드, 찍기 결과 카드 문구(`Shapes`·`Clean, crisp stamps.`·`Stamp all N shapes cleanly.`), 도감 스크롤 E2E 견고화. 유지한 것: 튜토리얼 `Release to stamp` 범위 0.25(끝에서 떼면 ★1), 떨림 시드가 레벨·찍기 번호라 Retry 시 같은 떨림이 반복됨(스펙 2.2, 바꾸려면 시드에 시도 횟수 추가). - 미룬 사소한 지적: `StageSession.submitStamp`의 `meshThickness ??` 폴백이 죽은 코드, 누름 impulse·`fitGuidesToRemainder`·빗나감 힌트 복귀의 Game 수준 테스트 없음, 빠른 경로 ↔ 느린 경로 동치 속성 테스트 없음, 진동 E2E가 벽시계(`waitForTimeout(2000)`)에 의존, `STAMP_TOUCH_LIFT`가 e2e와 Game에 이중 정의, 36레벨 두 달이 맞물려 태극 무늬처럼 보임(스펙 배치), 밝은 사물(코르크·비스킷·쟁반)에서 비활성 점선(0.45)이 흐릿함. - 실기기 확인이 남았다: Android Chrome에서 터치 띄움 80px 체감, 떨림 파동 진동([6]) 체감, 누름·찍기 손맛. iOS Safari는 진동 API가 없다. ## 이전 상태 (D2 구현 시점) - 레벨이 33개다(하이퍼캐주얼 전환 A~D의 최종 레벨 구성). 29~33은 조각 다시 자르기(오이·당근 2칼질, 토마토·파프리카·오이 3칼질). 처음에는 가이드 하나만 보이고, 자를 때마다 큰 조각 위에 그 조각을 넓이로 반 가르는 새 가이드가 그려지며 나타난다(직전 칼질 기준 90° → 뒤로 갈수록 60°·45° 비스듬). 첫 칼질이 빗나가도 실제 조각에서 계산한다. - 대상만 잘린다(다른 조각을 지나가도 그대로, 다른 조각만 그으면 칼질을 쓰지 않음). 카메라가 대상에 다가가고(최대 1.8배, 0.4초 동안 입력 막음), 나머지 조각은 채도를 덜고 배경색 쪽으로 옅어진다. 마지막 칼질 뒤에는 전체 보기로 돌아와 넘어진다. 29레벨에서 큰 `Cut again` 배지. - 테스트: 단위 859개 통과, E2E 408개 통과(기능 390: 기존 357 + 다시 자르기 27·28→29 진행 3·`Cut again` 배지 3, 성능 18, 원래 건너뛰는 3개 제외). 31레벨 세 번 자르기 준비 3.0~3.6ms·획 처리 3.2~3.3ms. - 시각 검수: 전체 시각 검수 2회차(데스크톱·모바일 × 10상태: 29 대기·포커스 이동 중·가이드 그려지는 중·포커스 끝, 31·32 두 번째 칼질 뒤, 33 넘어짐·결과, 28→29 떨어짐, 메인 화면). 1회차 흐림이 회갈색 덩어리 → 채도 45%↓ + 배경색 30% 섞기. 캡처 도구 `.playwright-mcp/d2/`(`capture.{config,spec}.ts`: `ROUND`·`ONLY`, `sheet.mjs`). ## 이전 상태 (D1 완료 시점) - 레벨이 28개다. 23~28은 회전판(오렌지·수박·키위·사과·레몬·용과, 가이드 1·1·2·2·2·3개, 평행). 판은 입력을 받을 수 있는 동안과 긋는 동안 돌고, 손을 떼면(빗나감 포함) 멈춰 연출이 끝나면 다시 돈다. 23레벨에서 큰 `Spinning cut` 배지. - 판은 절차 생성한 얇은 원판(밝은 도자기색, 가장자리 눈금 24개)이고, 젤리와 한 몸으로 쓸려 나가고 떨어진다. 카메라는 판 반경에 맞춰 조금 물러난다(모든 회전 각도에서 젤리 20px 여백, E2E). - 순수 로직(절단·채점·조각 다각형)과 셰이더는 회전을 모른다. 손을 뗀 각도로 획을 판 좌표로 되돌려 제출한다(단위: 4개 각도 × 23~28에서 ★★★, 25° 더 돈 각도로 되돌리면 ★★★ 아님). - 테스트: 단위 786개 통과, E2E 373개 통과(기능 357: 기존 312 + 회전판 45, 성능 16, 원래 건너뛰는 3개 제외). - 시각 검수: 전체 시각 검수 3회차(데스크톱·모바일 × 10상태: 23 대기, 24 회전, 25 긋는 중, 23 칼이 가장 깊을 때, 26 첫 칼질 뒤, 28 넘어짐·결과, 23 Retry 쓸려 나감, 22→23 떨어짐, 메인 화면). 1회차 모바일에서 판이 화면 밖 → 판 여유 0.12→0.08·카메라 기준 1.0→0.95, 판 색 대비를 올림. 캡처 도구 `.playwright-mcp/d1/`(`capture.{config,spec}.ts`: `ROUND`·`ONLY`, `sheet.mjs`). ## 이전 상태 (C3 완료 시점) - 16~22레벨 사물 7종을 자르면 단면에 속 구조가 보인다(놀라움은 엉뚱함이 아니라 그럴듯함, 사용자 선택). 통째로 재료인 부분은 재료색 반투명 젤리 + 결, 기계 부품은 불투명(사용자 선택 B). 어느 정답 방향으로 잘라도 핵심 부품(★)이 보인다(사용자 선택 A, 정답 직선 36개 단위 검사). - 프라이팬: 바닥 세 겹·옆벽·리벳 핀, 나무 손잡이 속 강철 탱·이음쇠, 뚫린 걸이 구멍 - 마우스: 기판(구리 층·칩)·AA 배터리 단면·광센서·마이크로 스위치·휠 - 망치: 강철 머리에 박힌 자루와 쐐기, 담금질 층, 고무 슬리브(C2 미룬 지적 1의 이음 경계도 맞춤) - 물총: 탱크 물·수면, 물관, 펌프 실린더·피스톤·오링, 연결관, 밸브, 방아쇠 스프링 - 열쇠: 진짜로 뚫린 고리 구멍·어깨 홈·양면 V홈·깎인 날, 황동 결 - 헤어드라이어: 팬·구리 모터·운모판 열선(발광)·전선·스위치 - 기타: 원목(앨더) 바디, 메이플 넥·장미목 지판·프렛·트러스로드, 픽업 코일·폴피스, 포트, 배선, 넥 포켓·나사 - 넘어짐 각도를 10°~22° → 16°~30°로 올렸다(단면이 읽히도록, 모든 레벨 공통). - 대상 밖 종류(과일·채소, 볼록 사물)의 셰이더는 C3 이전과 같다(기준선 해시 테스트). - 테스트: 단위 725개 통과, E2E 328개 통과(기능 312: 기존 270 + 단면 42, 성능 16, 원래 건너뛰는 3개 제외). 성능(부하 약 3.0~3.5): 16~22 획 처리 3.6~12.9ms·준비 2.1~3.0ms, 15레벨 준비 4.1~4.7ms·획 처리 10.1~10.2ms(한도 16·8ms). - 시각 검수: 전체 시각 검수. 사물마다 정답 각도 4~5개 × low·high × 데스크톱(필요 시 모바일)과 점검 빌드(조각을 70~80° 세운 단면 정면), 마지막에 16~22 모바일 low 45°·135°, 1~15 모바일 넘어짐 결과. 캡처 도구 `.playwright-mcp/c3/`(`capture.{config,spec}.ts`: `ROUND`·`LEVELS`·`ANGLES`·`QUALITY`, `inspect.sh`, `guide.{config,spec}.ts`, `sheet.mjs`). ## 이전 상태 (C2 머지·리뷰 미룬 지적 수정 시점) - 1~22레벨이 모두 다른 젤리다. 16~22는 프라이팬·마우스·망치·물총·열쇠·헤어드라이어·기타(반 가르기 함정이 뚜렷한 순서). - 새 사물 3종은 대칭을 깨서 쉬운 대칭 정답을 막는다(프라이팬: 손잡이를 0.07 비켜 달고 같은 쪽에 주둥이, 망치: 무거운 때리는 쪽, 열쇠: 한쪽 날에만 톱니). 반 가르기 규칙은 사물 목록(`SPLIT_GADGETS`)에도 걸리고, 주축 직선·눈대중 직선(`EYEBALL_LINES`)·두 조각이 안 되는 정답 각도 수(≤ 5/36, 망치는 0)를 검사한다. - 테스트: 단위 665개 통과(todo 없음), 기능 E2E 270개 통과. `npm test` 로컬 약 18~23초. - 성능: 획 처리 병목(같은 조각의 최대 내접원 두 번 계산)을 기억으로, 15레벨 준비 병목(볼록 캡 Bowyer–Watson, 조각 SDF 굽기)을 제약 삼각분할과 SDF 안쪽 반복 정리로 고쳤다. 2026-10-16 부하 평균 3.3~3.8에서 성능 프로젝트 16개가 3회 연속 통과했다. 16~22 획 처리 3.6~12.3ms·준비 1.7~3.1ms, 15레벨 준비 3.4~4.4ms(4배 CPU 제한 5.7~5.9ms)·획 처리 8.3~9.2ms(한도 16·8ms). - 시각 검수: 전체 시각 검수 2회차(데스크톱·모바일, 16~22 통째·결과). 캡처 도구 `.playwright-mcp/c2/capture.{config,spec}.ts`(`ROUND`·`LEVELS`)와 `sheet.mjs`·`crop.mjs`. ## 이전 상태 (C1 완료 시점) - 1~15레벨이 모두 다른 젤리다: 수박·오렌지·레몬·키위·딸기·사과·용과(1~7 과일), 토마토·오이·바나나·버섯·당근·배·파프리카·아보카도(8~15). 16~22는 C2 전까지 사물 4종 중복. - 테스트: 단위 600개 통과 + todo 1(16~22 사물 중복, C2), 기능 E2E 267개 통과. `npm test`는 메시 퍼즈를 묶음별 파일로 나눠 로컬 약 23초다(C1 직후 100초). - 시각 검수: 전체 시각 검수 11회차(데스크톱·모바일, 통째·단면). 캡처 도구 `.playwright-mcp/fruit-veg/capture.{config,spec}.ts`(`ROUND`·`LEVELS` 환경 변수)와 접촉 시트 `sheet.mjs`. ## 이전 상태 (B 완료 시점) - 레벨 1~22를 플레이할 수 있다. 젤리는 C 전까지 임시 배치라 중복된다(`tests/LevelRules.test.ts`의 중복 금지 `it.todo` 2개는 C에서 켠다). - 테스트: 단위 456개 통과 + todo 2개, 기능 E2E 249개 통과(데스크톱 Chromium, 모바일 Chromium, 데스크톱 WebKit). - 시각 검수: B는 데스크톱 1280×800 · 모바일 390×844에서 4회차까지 진행했다. 캡처 도구는 `.playwright-mcp/game-feel/capture.{config,spec}.ts`(gitignore)에 있고 재사용할 수 있다. A의 캡처 도구는 `.playwright-mcp/level-progression/`에 있다. - 실기기 확인이 남았다: Android Chrome 진동 체감·토글, 판정 문구·넘어짐·전환의 실제 손맛. iOS Safari는 진동 API가 없어 소리로만 피드백한다. ## 알려진 문제 - (해결) 15레벨 준비 시간 가끔 초과: 조각 SDF 굽기를 빠르게 해 3.4~4.4ms. 부하 평균 3.3~3.8에서 성능 프로젝트 3회 연속 통과(스펙 5.2절 기준인 "부하 3 미만"보다 높은 부하에서 통과). - (해결) 19레벨 기타 반 가르기 획 처리 한도 초과: C2에서 기타가 22레벨로 옮겼고, 16~22 모두 한도 안이다(최대 12.0ms). ## 미룬 사소한 지적 (B 최종 리뷰) — 모두 해결 (브랜치 `fix/feel-minors`) 1. 회전한 조각의 레이마치 좌표계: 시선(`vJellyView`)·키 라이트(`vJellyKey`)를 버텍스에서 조각 로컬로 돌려 넘긴다. 넘어짐·쓸려 나감 장면을 고치기 전후로 캡처해 확인했다(음영이 조금 바뀌었을 뿐 깨진 곳 없음). 2. 연출 중 HUD Home: 기억해 두었다가 연출이 끝나면 메인으로 간다(`homePending`, 그 사이 획은 받지 않음). 3. 흔들림 E2E: 칼질 전후 화면 좌표 불변을 단언하고, 착지 흔들림 도중 `ready`가 되는 경우를 따로 검사한다(복귀 코드를 빼면 실패함을 확인). 4. 슬로모션: 지금 수치로는 재현되지 않았지만(분리 이동 0.34초가 슬로모션보다 길다) `animating`을 벗어나면 리셋하는 불변식을 넣었다. 5. `Game.ts` 빈 줄 정리. 최종 리뷰(Opus medium): Critical·Important 없음. 리뷰의 Minor 4건(Home 대기 중 한 프레임 획 입력, 결과 카드가 한 프레임 보임을 스펙에 명시, 테스트 전제·한계 명시)도 반영했다. ## 미룬 사소한 지적 (C1 최종 리뷰) — 모두 해결 (브랜치 `fix/c1-review-minors`) 1. `npm test` 시간: 메시 퍼즈를 `tests/meshFuzz.ts`(`describeMeshFuzz`)로 옮기고 젤리 묶음별 파일 5개(`tests/JellyMeshFuzz.*.test.ts`)로 나눠 병렬로 돌린다. 볼록 과일·채소 10종은 추가 메스 시드(7919·65537)를 돌리지 않는다(오목 3종과 기존 10종은 그대로, 임계값도 그대로). 로컬 106초 → 23초, 테스트 620 → 600개. 2. `FEATURES`를 `Record`로 바꿨다. 한 종류를 빠뜨리면 `tsc`(빌드)가 실패함을 확인했다. 3. `JellyMaterial.fieldGlsl()` 주석에 테스트 전용임을 적었다. 4. `e2e/jellies.spec.ts`의 화면 안 검사를 가장자리 20px 여유로 맞췄다(세 기능 프로젝트 통과). ## 미룬 사소한 지적 (C1 지적 수정 리뷰, Opus medium: Critical·Important 없음) 1. (C2에서 해결) 퍼즈 묶음 합집합 검사: `FUZZ_GROUPS`·`JELLY_TYPES`. 2. `npx vitest list`(정적 수집)는 헬퍼 안의 `describe`를 못 봐서 묶음 파일 5개에 `No test suite found`를 낸다. `vitest run`과 `-t` 필터는 정상이다. 3. (C2에서 해결) `tests/meshFuzz.ts`의 연속 빈 줄. ## 미룬 사소한 지적 (C2 최종 리뷰) — 모두 해결 (브랜치 `fix/c2-review-minors`) 1. `JellyShapes.ts`의 사물 정의 사이 줄바꿈·빈 줄을 정리했다. 2. 옛 스펙 `2026-10-13-scalpel-curved-cut-design.md` 4.1절에 볼록 캡이 C2에서 제약 삼각분할로 바뀌었다는 줄을 달았다. 3. `principalAxis`는 면적 0 다각형에서 꼭짓점 분포의 주축(그것도 없으면 x축)을 돌려준다(NaN 없음, 테스트 추가). 4. 망치 자루-머리 이음의 오목 반경을 0.12 → 0.28로 키워 반 가르기 정답 직선이 튕기는 각도를 8/36 → 0/36으로 줄였다(함정 값은 그대로: 자루 중심선 77.1). 검사 한도를 9 → 5(기타·열쇠가 5)로 낮췄다. 시각 검수 3회차로 망치 모양 확인. 5. 15레벨 준비 시간: 위 성능 항목. 테스트 이름의 `[Review Focus N]` 접두어는 e2e 18곳에서 쓰는 기존 관례(계획의 Review Focus 항목을 가리킴)라 그대로 둔다. ## 미룬 사소한 지적 (C2 지적 수정 리뷰, Opus medium: Critical·Important 없음, 머지 가능) 1. (C3에서 해결: 자루 혀 밖의 필릿 살을 쇠머리 목으로 칠하고 이음 선을 혀 둘레로) 망치 자루-머리 이음 필릿(반경 0.28)이 셰이더 부위 경계(머리 x ≥ 0.46, 이음 선 |z| ≤ 0.16)와 어긋난다. 필릿의 x 0.22~0.46은 나무색 깃, 그 오른쪽은 강철로 칠해지고, |z| 0.16~0.28 구간의 나무-강철 경계에는 이음 선이 없다. 아래쪽은 못뽑이 안쪽 면(x ≈ 0.55)보다 왼쪽에 강철 띠가 생긴다. 미관 문제라 웹 확인 때 이음부를 본다. 2. `bakePieceSdf` 동치 가드 테스트는 재절단 조각(떨어진 두 절단에서 온 절단 변)과 절단 변 없는 조각을 다루지 않는다. 같은 분기로 이미 검증되어 실질 위험은 낮다. ## 미룬 사소한 지적 (C3 최종 리뷰, Opus medium: Critical 없음, Important 1건 수정 — 발자국을 그려지는 형상에 맞춤) — 모두 해결 (같은 브랜치) 1. 줄무늬(기타 코일·드라이어 모터·드라이어 팬 날개·마우스 휠 살·물총 스프링)를 상자 필터 `jStripe`로 그린다(필터 없는 `step(… fract(…))`가 `jInner`에 없음을 테스트). 모바일 low 캡처로 확인. 2. `CUTAWAY_TYPES`가 모두 solid + baked인지 테스트한다. 3. 테스트 도우미 `stripDefine`이 `#if defined(X)`·`#if !defined(X)`를 다루고, 대상 매크로의 `#elif`는 예외로 거부한다. 4. 발광은 `jLayEmit`로 더해 앞에 덮인 부품에 가려진다(마우스 LED 2곳, 드라이어 열선). 5. 열린 빈 공간의 옆면(외곽)으로 빠져나간 시선도 배경을 받는다(열쇠 깎인 날). 6. 문서 어긋남 3건을 고쳤다(B 판단의 넘어짐 각도 표시, 스펙 6.1 검사 레벨, 스펙 3.7 포트 3개). 7. 발자국 상수 사용 테스트는 GLSL 주석을 지우고 본다. ## 미룬 사소한 지적 (D1 최종 리뷰, Opus medium: Critical 없음, Important 1건 수정 — 긋는 동안 판이 96° 돌아도 손 뗀 각도로 자르는 E2E) — 모두 해결 (같은 브랜치) 1. 계획 Review Focus 1 문구를 실제 동작에 맞췄다(캔버스 밖에서 떼면 포인터 캡처라 정상 절단, 자르지 않는 것은 `pointercancel`·`lostpointercapture`). `lostpointercapture` E2E를 더했다. 2. Retry E2E가 떨어지는 중간에 젤리가 착지 뒤보다 40px 넘게 위에 있음(판 그룹이 내려옴)과, 입력 가능해진 뒤 판이 다시 도는지를 단언한다. 3. 25~28에서 마지막 하나를 남기고 자른 뒤, 벌어진 조각이 도는 12초 동안 화면 20px 여백을 검사한다. 4. `Game.loadLevel`이 메스 + 회전판 레벨을 거부한다(메스 연출·칼자국에 판 좌표 변환이 없음, 테스트). 5. 판 회전 식을 `geometry/Vec2.ts`의 `rotateY`로 옮겼다. `Spin.toWorld`와 `TipPose`가 이것을 쓴다(TipPose → stages 의존 제거, 테스트). ## 미룬 사소한 지적 (D2 최종 리뷰, Opus medium: Critical 없음, Important 2건 수정) — 모두 해결 (같은 브랜치) 수정한 Important: (1) 3칼질 레벨 두 번째 칼질 때 자르는 조각이 흐려졌다가 교체 순간 밝아짐 → 흐림 목표를 포커스를 정할 때만 조각별로 정하고 새 조각은 그때까지 유지(E2E: 31 칼질 연출 중 자르는 조각·새 조각 흐림 < 0.05), (2) 31~33 두 번째 포커스의 20px 여백·화면 방향 전환 E2E 추가(결함 없음). 1. 움직임 줄이기에서는 카메라만 즉시 다가가고, 흐림은 그대로 0.4초에 걸쳐 바뀐다(마지막 칼질 뒤에도 흐림이 풀릴 때까지 기다림, E2E). 2. 다음 가이드를 만들지 못하면(넓이 0 등) 대상 제한을 푼다(가이드 없는 대상만 남지 않음, `vi.mock` 단위 테스트). 3. Retry·Home 뒤 흐림이 0인지 E2E가 `pieceDim`으로 단언한다. 4. `Game.focusRecut`은 대상을 먼저 확인한 뒤 가이드·흐림을 건다. 5. 새 가이드가 그려지는 시간을 `GUIDE_REVEAL_SEC`(0.35초)로 빼고, `revealOf`·`ids`에 테스트·진단용 주석을 달았다. ## D2에서 사용자 대신 내린 판단 - 첫 가이드 위치: 29 오이 x = 0.15, 30 당근 x = 0.15, 31 토마토 x = 0.12(세로), 32 파프리카 (0.1, 0)에서 25°, 33 오이 (0.15, 0)에서 30° 기울임(가운데에서 조금 비켜 큰 조각이 분명하게). - 흐림은 어둡게 누르지 않고 채도를 덜어 배경색 쪽으로 물린다(계획의 처음 값은 회갈색 덩어리로 보였음). - 포커스 배율 = min(1.8, 원본 반경 / 대상 조각·새 가이드 끝 반경), 중심 = 대상 외접 사각형 중심. 포커스 0.4초 동안 입력을 받지 않는다. - 새 가이드가 그려지는 시간 0.35초(감속). - 흐림 유니폼은 모든 젤리 셰이더에 들어가고, C3 기준선 해시 테스트는 그 선언을 빼고 비교한다(스냅숏 값 불변). ## D1에서 사용자 대신 내린 판단 - 빗나간 획도 손을 떼면 판이 멈춘다(탭은 멈추지 않음). 판은 그릇처럼 젤리와 함께 떨어지고 쓸려 나가며, 메인 화면에서는 멈춰 있다. - 23~28 과일·가이드 배치(스펙 2.3절 표), 평행 가이드 끝점은 외곽 + 0.04. - 판 두께 0.05만큼 젤리·획 투영 평면·칼 높이를 올린다. 칼날이 판을 뚫어 보이지 않아 칼 바닥 높이는 바꾸지 않았다. - 판 여유 0.08(계획의 0.12에서 줄임), 카메라 기준 반경 0.95. - E2E 전용 디버그 `setPlateFrozen`(회전 시계만 멈춤)과 도우미 `freezePlate`. ## C3에서 사용자 대신 내린 판단 - 대상은 16~22의 사물 7종. 로봇·키보드·카세트(레벨에 안 씀)와 과일·채소(C1에서 실제 단면)는 제외. - 사물별 부품 목록(스펙 3절)과, 빈 각도를 채우려고 더한 부품: 마우스 배터리, 기타 넥 포켓·배선 통로·컨트롤 홈, 열쇠 어깨 홈, 물총 펌프 연결관(구현 중). - 기타 바디를 체리 젤리 → 원목색 젤리, 망치 자루 속 나무 심 제거, 물총 노란 물관 → 투명 관 속 물. - 넘어짐 각도 16°~30°(스펙 5절 허용 상한). - 단면 평면 패스, 깊은 곳 세부 무늬 생략(`jDeep`), 절단면 반사 줄이기, 통재료 면 반투명 덮기, 빈 공간 안 껍질 띠 없음. - 물총·헤어드라이어·프라이팬 기름·마우스 젤리를 조금 진하고 탁하게(밝은 젤리는 톤 매핑에서 희게 날아가 부품을 덮음). ## C2에서 사용자 대신 내린 판단 - 사용자가 설계·계획·구현·검토를 승인 없이 진행하라고 위임했다. 실행은 Native, 시각 검수는 전체, 머지·푸시는 하지 않고 검토까지. - 16~22 배치 순서(16 프라이팬 … 22 기타). - 주축 직선 검사만으로는 기울인 대칭 사물을 못 막아 사물별 눈대중 직선(`EYEBALL_LINES`)을 더했다. - 프라이팬은 주둥이만으로 대칭이 안 깨져(손잡이 선 90.2) 손잡이를 0.07 비켜 달았다. - 망치 못뽑이의 V 홈은 필릿 규칙상 외곽으로 낼 수 없어 셰이더 틈 무늬로 그렸고, 못뽑이 안쪽 면을 곧게 펴 오목 주머니를 없앴다(반 가르기 정답 직선 36개 중 17개가 거부되던 문제). 거부 각도 ≤ 9/36 검사를 더했다(C2 리뷰 수정에서 망치 0/36, 한도 5로 낮춤). - 새 3종 퍼즈는 별도 묶음(`solidKitchen`)으로 나눴다(한 파일이 1.5배를 넘음). - 성능은 외곽 원호 간격(`arcStep`)을 줄이지 않고 계산 중복 제거와 캡 삼각분할 교체로 맞췄다(외곽·함정·시각 재검수 불필요). 볼록 조각의 캡 삼각형 배치가 바뀐다(절단 결과·외곽은 같음). ## C1에서 사용자 대신 내린 판단 - 둥근 과일·채소 외곽은 계획의 제어점 16개·고정 반경 대신 24개 + 인접 변 길이에 맞춘 자동 필릿 반경으로 만든다(다각형처럼 각져 보였음). - 바나나 씨는 스펙의 "3갈래" 대신 몸통 중심선의 작은 점 한 줄이다(보이는 면이 세로 단면이라 3줄은 지퍼처럼 보였음). 스펙 표도 고쳤다. - 오이·당근 무늬는 길이 축을 원기둥 축으로 보고 단면 평면에 그린다(가로로 자른 면에 3갈래 씨·동심원, 위에서는 길이 방향 줄). - 버섯 갓은 윗면에 두꺼운 갈색 층을 둔다(외곽 띠만으로는 흰 T자로 보였음). ## B에서 사용자 대신 내린 판단 - 결과 카드가 HUD Home을 덮고 있어, 결과 상태의 Home E2E는 키보드 경로와 같은 DOM click(`pressHudHome`)으로 누른다. 결과 카드에 마우스로 누를 Home 버튼은 없다(A 때부터의 동작). - 판정 문구가 뜨면 시작 배지를 내린다(모바일에서 같은 자리를 다툼). - 판정 문구는 절단 지점 대신 젤리 윗면 바로 위(가로는 절단 경로 중간)에 띄운다. - 넘어짐 각도는 10~22°(C3에서 단면 속 구조가 읽히도록 16~30°로 올림)이고, 넘어질 때 밖으로 밀리는 윗면 모서리의 절반만큼 조각을 안쪽으로 당긴다(`TRANSITION.tipPull`). 떨어지는 동안 가이드를 숨긴다. ## 작업 방식 메모 - 실행은 작업 단위에 맞춰 Native/서브에이전트 구현을 사용한다. 최종 독립 리뷰 기본값은 사용자 전역 지침의 Opus 5.5 / medium이며, 과거 A~D의 Opus 리뷰 결과는 당시 기록이다. - 시각 판단이 들어가는 계획은 구현 전에 "기초 구현 + 사용자 시각 조정"과 "전체 시각 검수" 중 하나를 고른다(A·B는 전체 시각 검수). - 구현은 현재 폴더의 새 브랜치에서 하고, **main 푸시 = 운영 배포**이므로 사용자가 요청할 때만 푸시한다. - E2E는 CI에서 돌지 않으므로 푸시 전에 로컬 `npm run e2e`로 확인한다. ## 다음 할 일 0. R 구조 리팩토링: 0~8단계 완료·main 머지(마지막 `a65ecf4`, 6·6.5단계 `079db48`). 푸시는 사용자 지시 대기. 남은 작은 정리는 위 R 7·8단계 절의 'R 이후 후보'. 0. H 휘둘러 베기: 사용자 시각 조정(`?level=46`~`58`, 목록은 H 절) → 최종 독립 리뷰(Opus 5.5 / medium) → 실기기 진동·슬래시 속도 체감 → (요청 시) main 머지·푸시·배포. `audio.spec` 6개는 효과음을 다시 켤 때 고친다. 0. 앱 전환 1 뒤: Jenkins iOS·Android 빌드 준비 완료(2026-10-20, `docs/jenkins-pipeline.md`: 잡·업로드 키·자격 증명 등록, 같은 스크립트로 release AAB·APK·IPA 생성 확인, APK는 S22 설치·실행 확인, Jenkins 화면에서의 첫 실행과 App Store Connect·Play Console 앱 등록은 사용자) → **앱 1.5 `Game.ts` 리팩토링**(동작 변화 0, 단독 브랜치) → **앱 2 수익화 연동**. 앱 소리 다시 잡기, iPhone 실기기 체크리스트는 별도. 출시 전 썰기 소리 권리·스토어 등록 자료(진짜 아이콘·스플래시)는 별도. 1. G 천천히 썰기: 최종 독립 리뷰(Opus 5.5 / medium) → 사용자 이해도 확인·Android 실기기 진동 체감·청음 → (요청 시) main 머지·푸시·배포. 출시 전 썰기 소리 권리 확인. 2. Jelly Slice 홈 개편을 로컬 미리보기에서 사용자 확인. 최종 독립 리뷰까지 완료했고 ASMR 선행 작업과 함께 현재 작업 폴더에 보존했다. 3. (요청 시) main 머지·푸시·배포, 웹에서 새 홈·히스토리·도감·설정과 23~28 회전 속도·난이도, 29~33 다시 자르기 포커스·흐림 확인. 4. 실기기(Android Chrome, iPhone Safari)에서 B 손맛, C 젤리, C3 단면 패스의 모바일 GPU 프레임, D 회전판·다시 자르기 손맛 확인(자동 측정 없음). 5. 하이퍼캐주얼 전환 A~D 완료 뒤: 곡선 칼질 기믹 레벨(D 스펙 1.2절, 이후 스테이지), 앱 포장(E) 시점 판단.