데이터 복구 배포 증거를 동기화

This commit is contained in:
Yun Chan 2026-08-29 16:12:24 +09:00
parent dba9b75a38
commit ffadc8bd49
7 changed files with 134 additions and 14 deletions

View file

@ -983,6 +983,12 @@ DB 레벨 이중강제(`04_audit_eval_rls.sql` §5, `app/db.py` `acquire()`):
`AUTH_EMAIL_COHORT_MAP``AUTH_DOMAIN_COHORT_MAP` 설정, SAML fixture의 `cohort` claim을 합쳐
`cohort_ids`로 세션에 저장한다. 관리 사용자 `app_user.external_id`는 provider subject 기반
(`google:`/`saml:`/`dev:`)으로 저장해 email 변경 리스크를 줄인다.
- 한 사람이 복수 Google subject를 가져야 할 때는 소유자가 검증한 `app.auth_identity_alias`
provider subject를 canonical `app_user`에 연결한다. callback은 이메일 조회보다 이 별칭을 먼저 해석하고,
역할·코호트·온보딩·관리자 권한과 학습 데이터는 canonical 사용자에서 읽되 `auth_session.login_email`에는
실제 로그인에 사용한 검증 이메일을 별도로 남긴다. 같은 이메일이라는 이유만으로 계정을 자동 병합하지
않으며 inactive/suspended canonical 대상은 fail-closed한다. 별칭 테이블은 RLS SELECT 정책만 두어 런타임
역할의 쓰기를 막고, 연결·해제는 owner-run migration/복구 절차와 감사 로그로만 수행한다.
- 관리자 외부 연구참여자 사전등록 `POST /admin/users`는 요청값과 무관하게 항상
`account_status=pending`으로 정확한 이메일 계정을 만든다. 승인·정지는 별도
`PATCH /admin/users/{user_id}`에서만 수행해 create와 승인 권한 효과를 분리한다. 이는 공개 무제한 가입

View file

@ -98,9 +98,10 @@ python -m pytest engine_gateway/ --collect-only -q
| REQ-007 | session persistence/evaluation 관련 API 9 files, live/session 묶음, 실제 DB persistence E2E | 163 + 50 pytest, DB E2E 1 passed — 실패한 옛 평가 뒤 새 회기 가능 |
| REQ-008 | `app/test_engine_health_contract.py`와 session stream 회귀 | health contract 1 passed; incomplete EOF는 live/session 50 묶음에 포함 |
| 학습자 성장 패널 헤더 | `uc-learner-home-dashboard.spec.ts --grep "성장 지표 라포 헤더"` + `check:design-ssot` + `check:cosmetic-filter-safety` + typecheck | chromium desktop/mobile 2 passed — 선택된 라포 문구의 부모는 plain `div`, 배경 transparent·border/radius/padding 0이며 주변 panel 구조는 유지 |
| REQ-001 공개 운영 | `https://vignette.chanpaca.net/login` + Google account chooser/callback | 로그인 DOM ‘모든 Google 계정’, `allowed_email_domains=[]`, twentyoz.kr/Gmail 동시 노출, 기존 Google 계정 callback→`/admin` 성공. 신규 Gmail 선택은 계정 생성 행동시점 확인 뒤 별도 실행 |
| REQ-001 공개 운영 | `https://vignette.chanpaca.net/login` + Google account chooser/callback | `allowed_email_domains=[]`이며 기존 일반 Google 계정 callback→`/admin` 성공으로 가입 범위는 확인했다. Pages production `ef48c0ae…` 승격 뒤 custom domain은 단일 `Google 계정으로 로그인` CTA 1개·구 계정 선택 버튼 0·dev login 0·console warning/error 0이고 격리 Playwright OAuth 시작 검증 **1 passed**다. 실제 Gmail callback과 복구 계정의 `/admin`·기존 데이터 확인 전에는 검토를 유지한다. |
| Google 계정 데이터 복구 | migration 19, alias/auth focused, 전체 API·gateway, 백업 복제 DB, 활성 DB·공개 API | Gmail의 빈 중복 사용자와 canonical 관리자 계정을 이메일 자동병합 없이 명시적 Google subject 별칭 2개로 연결했다. 정식 계정 21회기·64턴·평가 6건과 Gmail 원본 설정 1행을 보존하고 source-only 알림 키를 canonical 설정에 무손실 병합했다. Gmail 옛 세션 1개 철회, 중복 계정 suspended, 감사로그 1건, 사용자 1175·회기 469·평가 65와 고아 0을 확인했다. API+gateway **1111 passed/1 skipped**, release agent **31 passed**, 백업 복제 DB 트랜잭션·cleanup 및 런타임 DB 역할 alias SELECT를 통과했다. 실제 Gmail OAuth callback은 사용자 계정 선택 후 최종 확인한다. |
| REQ-005·007·008 공개 운영 | P20 회기 `03dddadd-adf3-4922-acbf-31002470da52` | 생성→자기점검 원장 잠금→실제 학습자/AI 내담자 2턴→종료→AI 평가 완료→P20 1회 기록 영속→다음 회기 버튼 재노출 |
| 운영 배포·복구 | live clean `2a396361…89c`, Pages `1f1ddf18-9d5c-4d83-95a7-8cd71245e646`; recovery task `99779a6a…fbc8` | local/public prod·db·engine·voice·web 정상. recovery root/tree/script pin, 60분·IgnoreNew·mutex·hard-down 즉시복구 계약 50 passed; watchdog·boot 수동 실행 결과 0. 실제 Windows 재부팅 smoke와 새 Pages 승격은 미실행 |
| 운영 배포·복구 | live API clean `dba9b75a…`·tree `14cd4607…`, Pages `ef48c0ae-374e-41fc-b6fd-03f69b26e946`; boot/watchdog 동일 pin | fresh API+cloudflared provenance receipt SHA `186529be…f7a4`, 실제 API cwd가 새 detached root이며 local/public prod·db·engine·voice, OpenAPI 129, auth 401이 정상이다. boot/watchdog 실제 실행은 `LastTaskResult=0`·failcount 0이고 custom domain Pages entry SHA는 clean build와 일치한다. 배포별 preview origin은 정적 자산만 일치하고 연결 진단을 표시하므로 runtime GREEN이 아니며, 실제 Windows 재부팅 smoke는 미실행이다. |
로컬 Playwright는 기존 포트의 서버를 기본 재사용하지 않는다. 공개 preview나 다른 checkout의 stale bundle을
현 작업트리 증거로 오인하지 않도록 충돌 시 fail-closed하며, 동일 dev server를 의도적으로 공유할 때만
@ -551,6 +552,7 @@ hourly heartbeat는 최신 GREEN이 6시간 이상 오래됐거나 material mile
- 2026-07-15 보태니컬 글래스 UI·SSOT 검증: `npm run check:design-ssot` + `npm run typecheck` + `npm run build` 통과, `npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run --reporter=line` 시각 게이트 **15/15**, `npx playwright test e2e/auth-visual.spec.ts --project=chromium-single-run --reporter=line` **1/1**, `npx playwright test e2e/session-layout.spec.ts e2e/learner.spec.ts e2e/session-review.spec.ts --project=chromium-desktop --project=chromium-mobile` **46/46**, 로그인→온보딩 focused desktop/mobile **2/2**. 공통 AppShell GNB, Theme store, Surface variant의 소유권과 전 폭 대시보드/리뷰 탭, 로그인·온보딩 라이트/다크 390/1280px, 고해상도 보태니컬 자산을 함께 고정한다. 라이트 테마도 패널당 복수 굴절 그라데이션, backdrop blur, 헤어라인, 그림자를 computed style로 단언하며 모바일 셸이 보태니컬 배경을 제거하지 않는지 검사한다. 관리자 사용자 표는 semantic table, 정렬 헤더, 1440px 최소 폭과 표 전용 가로 스크롤을 desktop/mobile에서 검증한다.
- 2026-07-31 활성 세션 보태니컬 워크스페이스 검증: `full-sweep-session.spec.ts`의 1536×1024 계약이 좌 326px·우 357px 레일, 1408px 상단/본문, 1468px 하단 제어바, 좌우/스테이지 보태니컬 WebP 연결과 라이트 테마를 실측한다. 세션 전수 desktop/mobile **32 passed / 2 skipped**, `session-layout` **8/8**, 7폭 `layout-visual-gate` **15/15**, `check:design-ssot`·typecheck·build를 통과했다. 1366×768 이하는 스테이지보다 자막이 작아지지 않게 별도 압축 계약을 적용한다.
- 2026-07-31 관리자 Provider·모델별 비용 원장 검증: `python -X utf8 -m pytest -p no:cacheprovider app/test_llm_pricing.py app/test_admin_ops.py app/test_usage_report.py engine_gateway/test_provider_registry.py engine_gateway/test_gateway_model.py -q` **66 passed**. Claude CLI SDK 비용 추정값과 Agy/Gemini·Codex·Claude API 공식 참조단가 추정을 분리하고, 기존 DB의 0달러 Agy 행을 조회 시 재산정하며, 단가 미등록 모델은 `미산정`으로 남기는 계약을 고정했다. `npm run generate:api-types`·`check:api-types`·typecheck·build를 통과했고, 관리자 AI desktop/mobile focused E2E **2 passed**와 7폭 focused layout visual gate **1 passed**에서 Gemini 원장 `$0.06`·`참조단가` 표시와 레이아웃 containment를 확인했다. 공개 API 프로세스를 재시작하고 Cloudflare Pages production `11b3e11f`에 배포했다. 인증된 공개 `/admin/usage?window_days=30``source=database`, `durable=true`, `gemini-3.6-flash-high` 5호출·입력 35,703·출력 1,129·참조단가 `$0.062022`를 반환했고, 검증용 인증 세션은 즉시 삭제했다. 커스텀 도메인의 `AdminAi-BfxMUlzj.js``AdminAi-XHkda_d4.css`는 올바른 JavaScript/CSS MIME으로 200을 반환한다.
- 2026-08-29 Google 계정 데이터 복구 검증: migration 19가 owner-managed `auth_identity_alias`와 세션의 별도 `login_email`을 추가한다. Gmail 중복 계정은 학습 데이터 0건이지만 기본 설정 1행이 있어 최초 가드가 rollback했고, 전체 app_user FK를 전수 스캔한 뒤 canonical 설정과 겹치는 값이 동일함을 확인했다. 개정 트랜잭션은 source-only `account_approval` 알림 키를 canonical 설정에 합치고 원본 설정 행은 보존한다. 백업 복제 DB에서 alias 2·세션 21·턴 64·평가 6·활성 source session 0·고아 0으로 통과 후 복제 DB를 삭제했고, 활성 DB도 사용자 1175·회기 469·평가 65·고아 0을 유지했다. 공개 API fresh provenance는 commit `dba9b75a…`·tree `14cd4607…`, health DB/engine true, voice exact, OpenAPI 129, auth 401이며 boot/watchdog 실제 실행 결과는 0/0이다. Pages production `ef48c0ae…`의 custom domain은 clean build entry·login chunk SHA와 일치하고 단일 Google CTA E2E 1건을 통과했다. 실제 Gmail callback은 브라우저 계정 선택을 기다린다.
- 2026-07-31 Claude CLI 토큰 원장 focused 검증: result `modelUsage`의 전체 agent tree를 합산하고 최상위 `usage` 폴백, cache read/create 입력 포함, generate·SSE done 전파를 회귀화했다. 전체 backend **436 passed**, gateway **45 passed**, API type generation/check·typecheck·build, 관리자 AI desktop/mobile **2 passed**. 신규 live Opus generate는 입력 **31,918**·출력 **4**·비용 추정 `$0.124862`, Haiku SSE done은 입력 **30,450**·출력 **159**·비용 추정 `$0.061685`를 반환했다. 운영 DB 30일 원장의 과거 Claude 행은 역산하지 않고 `tokens=미계량`, `token_unmetered_turns=194`, `cost_basis=provider_estimate`로 분리됨을 임시 인증 세션으로 확인하고 세션을 즉시 폐기했다.
- 2026-07-31 Claude CLI 과거 토큰 백필 검증: 로컬 Claude JSONL **3,908개**의 assistant usage record **3,848개**를 읽되 본문을 출력하지 않고, 정규화 응답 SHA-256과 DB 생성 시각 창(-30초~+180초)이 모두 일치하며 후보가 정확히 1개인 턴만 복구했다. 운영 DB의 과거 0/0 Claude 턴 **442건****169건**을 실제 입력 **5,179,999**·출력 **46,910** 토큰으로 갱신했고, 불일치 **273건**은 계속 `미계량`, 모호한 후보는 **0건**이었다. apply는 `--expected-matches 169` 가드와 행별 `UPDATE 1` 확인을 통과했고, 적용 후 dry-run 재실행에서 추가 exact match **0건**을 확인했다. 전체 backend **439 passed**, gateway **45 passed**이며, 공개 관리자 브라우저 **1 passed**에서 Claude 행의 실제 토큰과 부분 계량 `125/183회` 표시를 확인하고 임시 세션을 폐기했다.