R02 — 구·신 앱/서버·저장 장애 호환 판정
앱 #1541, 리팩터링 5-1. 2026-09-11 KST에 R02가 직접 실행했다. 앱 PR#1577은16:13:55에release/v0.18.0으로 병합됐다(4fe99af0aa1df7882cb6007129933bba65ef4875). 문서 PR#73에 최종 근거와 BUG-122·BUG-123을 연결한다. 앱 작업 위치는 D:/LiftGuild-2026/Claude/wt1541, 작업 브랜치는 codex/1541-r02-compat, 세션은 01a08aec-4a3f-7343-b794-a48056c4b9f0다. 선택 검사와 release 반영은 전체 호환 승인·Full CI·Production 배포와 다르다.
기준 후보와 실제 배포 바이트
직접 선행 R01 PR #1569의 merge 849191b54b15682fba001dbde9757f88b76cf580이 release/v0.18.0에 포함된 후 작업을 시작했다. S08 06db380c477d53746e692f495e5fdfb66650e9c0, S09 2518e156b366fe0aaaad9b05309c88a2ee8b487d, N01 0102ec07b9007d02f2262b5dd1236e833413d05a, B01 ba0f403535075d53d1819b2021110376c5da6f41도 실제 조상 포함을 확인했다. 기다리던 자동 확인은 착수와 함께 중지했다.
| 구분 | 정확한 입력과 한계 |
|---|---|
| 구앱 | v0.17.1, 58e9b047eea319e2b1bae986d355ab5158d41c91의 실제 Deploy run 34122817046 |
| 원 배포 | https://barbelic-ayq5sijmp-dekerds-projects-4f9780ee.vercel.app; 원 Deploy 로그의 DEPLOY_URL과 일치 |
| 배포 파일 | 93개를 HTTP로 그대로 읽고 SHA-256 고정. 파일별 원본 해시. artifactKind=deployed-artifact, entry /assets/app-BlGYcGso.js |
| 구서버 | 같은 v0.17.1 태그의 migrations로 만든 격리 PostgreSQL. 장부 꼬리 20260913010100, 신규 restore 함수 없음 |
| 신서버 | R01 merge의 migrations로 만든 격리 PostgreSQL. 장부 꼬리 20260914213000 |
| 신앱 후보 | 최초 R01 merge, 저장 수리 후 413cdea3c969731e0cb1051e09d73a57416be2ff의 실제 Vite 빌드 140파일 해시. 과거 Production 배포 artifact였다고 표시하지 않음 |
| 실행 환경 | Windows, Node24.16.0, Supabase2.113.0, Docker29.7.2, PostgreSQL17.6.1.127, Playwright Chromium |
원 배포에서는 정적 파일만 읽었다. Production 인증·DB 요청, 사용자 백업이나 실제 사용자 기록은 사용하지 않았다. 모든 쓰기는 로컬 서비스/임시 계정이다. release 자산 목록과 Actions에는 앱 번들 archive가 없었지만, 원 Deploy의 불변 URL이 살아 있어 배포 바이트를 확보할 수 있었다. R01 문서 게시 당시의 artifact 미확보를 소급해 바꾸지 않고 이후 확보 증거로 추가한다.
명시적 E2E_DEPLOYED_LEGACY_BUNDLE을 주면 태그/commit/HTTPS provenance/파일 전체 집합/파일별 해시를 검증한다. 부적합하면 실패하며 source rebuild로 대체하지 않는다. 환경변수가 없을 때 기존 CASE039/040의 source-build 도우미 동작은 보존하지만, 그 실행을 실제 과거 배포 증거로 계산하지 않는다.
네 조합의 최초 실측
기존 CASE023의 사용자 입력→완료 자동 저장→메모→두 차례 수정→재진입→DB/정리 oracle를 재사용했다. 조합마다 --repeat-each 2로 서로 분리된 브라우저 context/임시 계정을 사용했다. 한 context의 두 탭은 별도의 CASE039/040에서, 같은 복원 요청을 가진 두 독립 context는 아래 실제 복원 검사에서 확인했다.
| 앱 / 서버 | 실제 결과 | 허용 판정 |
|---|---|---|
| old / old | 2/2 통과 | 기존 기본 저장 여정 허용. 신규 undo는 이 앱/서버의 기능이 아님 |
| old / new | 2/2 통과 | 기존 기본 저장 여정 허용. 실제 구앱/신앱 탭 공존 CASE039/040도2/2 |
| new / new | 2/2 통과 | 이 기본 여정 허용. 저장 장애/생애주기 결과는 아래 별도 판정 |
| new / old | 2/2 실패 | 전체 여정 허용 불가. 신서버가 준비되기 전 신앱 제공을 호환 성공으로 인정하지 않음 |
new/old의 실패 지점은 알림 list_user_notifications_v1, 그룹 list_my_groups_design_v1, 초안 get/save/clear_workout_draft_checkpoint_v2 부재의 404가 사용자 오류 게이트에 잡힌 것이다. 그 직전 실제 canonical readback은 completed/revision1/60kg×5 두 세트로 통과했다. 따라서 canonical 생성 자체의 원자성 결함이라고 단정하지 않는다. 게이트 이후 메모·두 번 수정·재진입은 실행되지 않아 성공을 주장할 수 없다. API fallback이나 오류 게이트 완화로 이 조합을 통과시키지 않았다.
구서버 undo는 현재 repository/binding을 실제 구 PostgREST에 연결한 독립 Chromium 두 context에서 확인했다. 목록/계획 모두 실제404/PGRST202, 미리보기 실패, 확인은 plan_not_ready, 적용/fallback 호출0, 추가 outbox0, localStorage 변화0이다. 최초에는 일반 오류 “확인하지 못했어요.”라서 정확한 안내 조건이 미충족이었다. d9ddf691cd2c3ed1ba40e121d3dc5fdd8b4bb40e에서 해당 복구 RPC signature 부재만 unsupported로 구분해 **“현재 서버는 이 되돌리기 기능을 지원하지 않아요. 서버 업데이트 후 다시 시도해 주세요.”**라고 안내한다. 인증·오프라인·5xx·일반404는 기존 의미를 유지한다.
최종 실제구서버 검사1/1에서 모바일·데스크톱 실제컴포넌트를 markup으로 그려 안내 가시성과 확인버튼 부재를 확인했다. 이는 전체 앱 shell/React effect 생애주기 검사가 아니다. 초기 outbox가 비어 있는 이 검사를 기존 원문 보존의 단독 증거로 확대하지 않으며, 기존 요청의 원문/후속 의도/owner 보존은 별도의 실제 저장 장애 검사로 증명한다.
저장소·원문·요청 정체성
| 검사 | 실제 검증 계층 | 결과 / 해석 |
|---|---|---|
| CASE039/040 | 실제 구배포+신번들, 동일 context의 IDB/mirror, 실제 신DB | 2/2. 구행 읽기·전송 중 후속 의도·legacy ACK 공존 |
| idbVersionPin/outboxAtomicity/Claim/Conflict/MirrorRecovery/RecoveryReceipt | 실제 Chromium IDB·localStorage, RPC 모의 포트 | 23/23. blocked6→7, incompatible7→6, 교체 중단/abort/claim/fence/후속행/receipt |
| outboxTimeout/pendingSaveMirror | 실제 브라우저, 제어한 transport | 4/4. timeout·mirror 경계. 실제DB 결과로 세지 않음 |
| workoutGenerationConcurrency/sessionWriteDeltaSafety/userFactRestoreConcurrency/기존codec | 실제 PostgreSQL 동시 세션과 RPC 함수 | 최초21/21. revision 경합·원자 롤백·복원 재생·stale/owner |
| 새 codec 연결 검사 | 실제 Chromium localStorage/outbox/dispatcher→Node transport port→실제PG plan/apply/receipt | 응답유실 후 재시작/독립context/owner/staleundo. 전체codec파일 최종4/4 |
새 복원 검사는 실제 canonical80과 영수증이 존재함을 확인한 뒤 첫 적용 응답만 유실시킨다. 요청ID/planHash/planRaw를 그대로 보존하고 문서를 닫았다 다시 열어 같은 outbox를 읽는다. 재시작한 context와 독립 context가 동일 receipt로 수렴하며 apply1회·receipt조회3회·잔여 outbox0이다. 타 owner receipt는 null이고, 미리보기 뒤 최신70kg가 저장된 오래된 undo는 stale 상태/원문을 보존하며70kg를 덮지 않는다. 전체 UI나 브라우저 직접 HTTP 인증을 검사한 것은 아니다.
대표 결함 대조로 제품 dispatcher의 receipt 재사용을 잠시 제거했을 때 apply3 ≠ 1 단언이 실패했다. 원 소스를 즉시 복구했으며 이 결함 코드는 커밋하지 않았다. 첫 teardown에서는 stats cron과 Auth user FK cascade deadlock이 발생했다. 테스트 전용 정리를 owner fence→기존 projection run row→user삭제 순서로 맞췄고, 제품 SQL이나 재시도 예산을 바꾸지 않았다.
IDB clear/강제 새로고침으로 성공시킨 보존 경로는 없다. 일부 기존 mirror-loss 검사의 DB삭제는 선언한 저장소 소실 입력이다. blocked upgrade는 구 연결을 닫아 정상 진행하며, v7→v6 VersionError는 허용된 rollback 성공으로 세지 않는다. 현재 호환 rollback 판정은 같은 IDB6을 쓰는 실제 구·신 탭 공존 범위에 한정한다.
생애주기와 최초 실패 수집
authResume/receiptResourceConvergence/prExerciseStaleData와 draft lifecycle/checkpoint/SW generation/native auth의 선택47개는 통과했다. 최초 controller 준비실패12건은 U06이 필수 feedResource·현재volume resource 조립을 맞춘 뒤 해결했다. R02는 U06의 9f7fa381 정책과 abc9d5bb fixture 수리를 자기 브랜치에 소비했다. 모의 native bridge/가정한 provider 성공 경계는 실제 설치 앱 또는 실제 제공자 인증 성공을 의미하지 않는다.
일반 사용자 CASE017/029/041/042/043/044/045/046/048/049/051/058 최초 묶음은 5통과7실패였다. 성공은017/042/046/049/051이다. 실패를 모두 수집한 후,041/043/044/048가 찾던 구 hook을 현행 workoutPersistenceStatus의 정확한단계/문구로 맞췄다. 029/045의 추가 mutation과058의 draft_archive_overwritten은 동일시각 picker 초기화를 새 편집으로 판단한 mobileWorkoutFlow의 비교 방식이 원인이었다. 실제 입력을 바꾸지 않은 초기화가 operationId를 회전시켰다.
실제 저장시각·메모로 비교하고 UI메타/원문 snapshot은 보존했다. 새2회귀는 수리전2실패→기존포함13통과, 이전 저장형식에서 정확한 요청 재개1통과다. 실제 시간편집은 여전히 새 요청이며 expectedRevision을 보존한다. 실패7CASE만 다시 실행해 **7/7 통과(3.1분)**했다. 서버 개정번호를 올리는 정상 논리저장 계약, 기존revision 기대값, 입력 원문, receipt, 오류/cleanup 게이트와 시간예산을 그대로 지켰다.058은 경고억제가 아니라 불필요한 operation 회전 제거로 오류0이 됐다.
수리 빌드의 CASE023는 new/new2/2 통과, new/old2/2 동일404 실패로 다시 확인했다. old/old와old/new의 구 artifact/서버는 변하지 않아 기존 실행증거를 유지했다. 이 결과들을 전체53CASE·Full CI 통과로 합산하지 않는다.
native 설치 버전·실제 WebView·실제 외부 Google/Kakao/Apple 인증은 가용 환경이 없어 미검증이다. CASE049/051의 실제 로컬 계정과 provider callback 경계 모의를 외부 인증 전체의 증거로 표시하지 않는다. 오너 실기기 검증을 완료 게이트로 요구하지 않는다.
품질 기준과 최종 후보
품질15개에 따라 원 요구사항과 실제 계층, 고정artifact·독립계정, 상태barrier, DB canonical/receipt 독립 기대값, 실제 주입 횟수, owner·원문·중복 부작용, 필수 완료·정리, 대표결함 실패를 구분한다. 기존 CASE의 DB/오류/cleanup oracle와60초·기다림10초·retry0을 보존한다. scope 제한과 미검증이 있는 항목을 전수 품질 승인으로 포장하지 않는다.
U06 정책 소비 후 Phase3 npm run check:3710전체/3618통과/0실패/92skip,144872ms. 저장수리 Phase4 check는3713전체/3621통과/0실패/92skip,146618ms. skip은 실제DB 대상 미지정과 opt-in 구서버 검사 등을 포함하며 통과 수에 넣지 않는다. 첫 check는 repo 안 work/의 생성된 Playwright viewer U+FFFD 검사로 정적 단계에서 실패해 생성물을 repo밖 작업폴더로 옮겼다. 검사 코드를 완화하지 않았다. 드라이브간 이동이 sandbox junction을 따라 own추적파일까지 이동한 것을 즉시 발견하고, 변경 없던 경로를 HEAD로 복구하여 의도한 diff만 확인했다. 원격/사용자 데이터 변화는 없었다. 그 시점은 최종 precheck 전이며, 최종 통합 증거는 아래 별도 절에 기록한다.
최종 precheck의 최초 실패 (2026-09-11 14:56 KST)
후보 02f8c7a738b6e86ac14ce0cdcea0869a4720862a, base 849191b5, 실행 local-1789105651976-100820은 8분42초에 실패했다. 정적/unused/빌드/artifact와 단위 3621개(실패0·조건부skip92), DB reset·schema snapshot·pgTAP 121파일/2435assert, 저장/영수증·복원·세대·순차/기간·worker/maintenance 검사는 통과했다. 마지막 stats-isolated-convergence에서 create162ms 다음 update가 save_session_v5→write_session_children_v5→exercise_set_part 삭제 FK→user_exercise_set_observations의 교착(40P01)으로 실패했다. 최초 로그와 임시 DB 정리 완료를 HQ에 전달했다. 이미 R04가 수리 중인 통계 경계로 조율하며, 다른 담당자의 DB 변경을 중복 작성하거나 같은 후보 precheck를 반복하지 않았다.
그 전에 D09 와드업 전용 SQL 2개가 남은 것을 확인한 첫 precheck는 pgTAP 실행 전 중단했다. 02f8c7a7에서 내용 변경 없이 optional 경로로 이동한 후 위 실행을 시작했다. 이 중단은 SQL 실행 실패가 아니며, 제외된 2개를 실행하거나 통과로 세지 않았다.
U06의 파일 분류 공유 124b1e10을 소비한 2f0c4225와 R02 구서버 검사 이름만 추가한 8a97d068에서 strict 분류 검사는 2889분류·41제외·미분류0으로 통과했다. 실제 파일이 없는 U06 규칙2개는 다른 담당자의 파일을 복제해 채우지 않았다. 이는 파일 분류 결과이며 통계 실패를 해소한 증거는 아니다.
HQ 공통 수리 소비 (2026-09-11 15:30 KST)
HQ가 유효한 수리로 인계한 R04 646f71d65d3208bf52e3349b39816150522789e7을 ef0f378a로, R03 9b3696ccc7e480abc61da37132df5c8dc5a593dd를 83a93fef로 소비했다. statsOrchestration의 R03 cutover/독립 연결과 R04 검증 인증서·payload 변조·결과 저장 취소 단언을 모두 보존했다. R04의 장기 성능 전수 합격을 의미하지 않는다.
공유 제품 수리는 정규화 관측도 private compute에서 계산한 후 원자적으로 게시한다. 같은 게시 transaction·lease·불변 SHA-256 payload의 검증만 재사용하고, 동일한 public 행은 INSERT 앞에서 제외한다. 기존6초 게시 예산과 provenance/원본/영수증 경계를 유지하며 계산 결과를 저장하다 취소되어도 기존 실패/backoff에 기록한다. migration 20260914223000·20260914233000과 definitions/schema/contracts가 포함된다. 사용자 원본 수리나 Production 직접 DB 적용은 없다. R03 fixture 수리는 기존 worker advisory lock과 cutover 준비 행 잠금으로 실제 예약 작업과 테스트를 분리한다.
소비 후보 83a93fef의 새 격리 DB에서 관련4파일은42/42통과·0skip(117991ms)했다. 삭제·세트 제거 회귀 모두parentWait=false, delete/compute 완료다. 원래660세션/7920set 수렴도ok=true/cleanedUp=true로 통과했다. 생성77ms·수정57ms·삭제28ms, 후속 게시4871ms(wall4895ms), checkpoint1976개가 게시 전 비공개/세대 일치/취소 보존을 만족했다. 의도한 취소57014에서attempt1·backoffHeld·leaseRotated를 확인했고 원래6초 게시 예산을 유지했다. own stack r021541stats는 종료/no-backup을 확인했다.
그 후 U06이 인계한 배포 장부 최신migration 정렬 03dcf548을 5bcd65b6으로 소비했다. EXPECTED_LATEST_MIGRATION 한 줄을 실제꼬리20260914233000에 맞추고 check:deployment가 통과했다. 최종 precheck는 이 새 후보에서 수행한다. 앞서 기록한 네 조합은 원래 후보의 실행 증거이며 공유 SQL 수리 이후 전체 조합을 재실행한 것으로 소급하지 않는다.
최종 precheck의 별도 실패 2건 (2026-09-11 15:50 KST)
5bcd65b6/base849191b5의 local-1789108841383-40828은10분2초에 종료했다. static·unit3621(0fail/98skip)·DB reset/schema·pgTAP121파일2435assert·저장/복원/codec·worker/maintenance·원래660세션 수렴이 통과했다. 다만 statsRefreshScopeConcurrency의 원본판 변경 시completed_count 단언은0기대/1실제(3pass1fail), statsOrchestration의 legacy payload publish는failed기대/idle실제(순차 묶음31pass1fail)로 실패했다. 새 R04 회귀와 R03 cutover computed 검사는 통과했다.
실패 원인을 fixture라고 단정하지 않고 HQ/R04에 실제 두 단언과 첫 전체 로그를 인계했다. 후속660검사는CRUD62/49/20ms·ok=true/cleanedUp=true로 종료했고 own stack도 정리했다. 별도2실패 해소 전에는 같은 후보 precheck를 반복하지 않았다. 고정 실행 종료 후 R03 PR#1575의 release merge 607647efb894404e583df910153b2c0dae0e656e를 자기 후보에 반영했다. 당시 최종 성공/앱 PR/release 반영은 미완료였으며, 이후 수리·최종 통합 증거를 다음 절에 보존한다.
HQ는 이후 D04의 기존 public 관측표 대기가 실제로 원본판 캡처 전 private 표 준비에서 발생함을 확인했다. D11은 경쟁 publisher가 run 행을 먼저 잡아 조회가idle이 되는 경합을 재현했다. 기존 R02 실패 실행의 원PID는 확인하지 못했으므로 재현과 구분한다. 검증된 두 fixture 수리 ba1914d47946cb0ebdf5142af661be296012f14c를 최신release607647ef를 합친 후보에 09c3802b로 소비했다. D04는 원본판 캡처 뒤 함수 진입의 실제 advisory/blockingPID와 주입 원정의 복원, D11은 독립 경쟁 연결·단언까지run행 보유를 확인한다. 원래0/40001/재계산/completed 기대값과 예산을 보존했으며 제품SQL이나 성능 프로토타입은 포함하지 않았다.
소비 후보 09c3802ba42afe27bf3c225e238c9b8871a6119b의 새 격리 DB에서 두 파일 전체21/21통과·0skip(20433ms)를 확인했다. 주입 함수 원정의 복원과 경쟁 연결을 포함해 원래 단언을 유지했고, own stack종료/no-backup도 확인했다. 실패 두 건의 관련 검증이 끝난 뒤 이 새 후보로 필수 precheck를 재개했다.
최종 필수 precheck 통과
09c3802ba42afe27bf3c225e238c9b8871a6119b, base 607647efb894404e583df910153b2c0dae0e656e의 실행 local-1789110057207-113516은 10분30초에 모든 단계 통과했다. static/unused/build/artifact, unit3622pass·0fail·100conditional skip, DB reset/schema·pgTAP121파일2435assert, 실제 저장/영수증/복원, 앞서 실패한 D04/D11을 포함한 순차/세대 경계, worker/maintenance, 원래660세션 수렴이 통과했다. 마지막 CRUD91/67/33ms·ok=true·취소복구/세대/체크포인트 보존·cleanedUp=true 및 own sandbox종료/no-backup을 확인했다.
이후 앱 PR#1577을release/v0.18.0 base로 생성하고 clean/로컬HEAD=PRhead에서 자기 Merge Check를 요청했다. 실제 큐 run34573327344이success로 끝나16:13:55 KST에merge 4fe99af0aa1df7882cb6007129933bba65ef4875를 게시했다. PR 상태MERGED와 PRhead09c3802b·mergeSHA의실제release 조상 포함을 모두 확인하고 세션 제목을[v0.18.0 반영완료]로 변경했다. 이전 실패 후보의 결과를 지우지 않으며 이 precheck/Merge Check를 Full CI로 표시하지 않는다. staging/Production은 승격하지 않고 이슈는 열림을 유지한다.
R05 재현 인계
- 실제old artifact를 새 폴더에
node tests/browser/r02/captureDeployedBundle.mjs <directory>로 받거나, 인계된93파일을 그대로 검증한다.E2E_DEPLOYED_LEGACY_BUNDLE은 bundle.json과dist-vite의 부모다. 계정/trace 비밀을 문서에 복사하지 않는다. - 기존 ci-local의 격리 stack helper로 구태그/신release migrations를 각각 준비한다.
E2E_SUPABASE_URL/ANON_KEY/SERVICE_ROLE_KEY,E2E_LOCAL_JWT_SECRET,BARBELIC_DB_SANDBOX는 각 stack의 로컬 출력만 사용한다. E2E_APP_URL=http://127.0.0.1:<own-port>,E2E_PREVIEW_PORT=<own-port>,E2E_START_LOCAL_APP=1,BARBELIC_TARGET=local.E2E_OUTPUT_DIR은 repo밖 전용작업폴더를 권장한다. 최초 build를 생략하지 않는다.- 구앱이면
R02_DEPLOYED_APP_TAG=v0.17.1, 신앱이면 비운다.node --import tsx node_modules/@playwright/test/cli.js test --config tests/browser/r02/compatibility.config.mjs CASE-023 --repeat-each 2를 조합별 실행한다. 실패new/old를 자동 재시도하지 않는다. - 신서버에 CASE039/040은 canonical playwright.config와 명시적artifact로 실행한다. 나머지 선택 CASE도 실제 경로 selector로 실행한다. 제외007/018/055/056·Wadup/admin 전용 자동검사를 실행/복귀시키지 않는다.
- 구서버 capability는
R02_OLD_SERVER=1 node --import tsx --import ./tests/support/registerNodeTestBudget.mjs --test tests/browser/r02/oldServerUndo.test.mjs. 구 장부를 단언하므로 신서버로 대체할 수 없다. - 신서버 복원은
BARBELIC_REQUIRE_DB=1 node --import tsx --import ./tests/support/registerNodeTestBudget.mjs --test tests/db/userFactRecoveryCodecDecode.test.mjs. 정확한DB env와필수 플래그로 skip을 차단한다.
후속 R05는 실제 최종release에서 이 판정표의 허용·불가·미검증을 그대로 사용한다. 신앱/구서버 오류없는 전체여정 실패와 native/provider 미검증은 승격 판단에서 남는 제한이다. 신규undo는 실제서버 부재를 정확히 안내하고 확인/전송을 차단했다. staging/Production 승격 승인은 이 문서가 부여하지 않는다.