G0~G8 성과·동맹 측정 OS 작업 일괄 고정

8월 7일까지 워킹트리에만 남아 있던 미커밋 작업을 커밋한다. 여러 사본
폴더(worktree·clone)에 흩어져 있던 중간 스냅샷을 정리하기 전에 원본을
git 이력으로 고정하는 것이 목적이다.

- contracts/routes/services: measurement, outcome_trajectory, rupture_repair,
  deliberate_practice, calibration_transfer, supervision_research,
  multimodal_alliance, continuous_improvement 계열 신규 모듈과 테스트
- infra/db/init: 07~16 마이그레이션(측정 기반~calibration transfer 실행)
- apps/web: 세션 리뷰 카드·관리 화면·E2E 스펙 추가
- docs/ops: G0~G8 라이브 통합·배포·롤백 증거 문서와 evidence JSON/PNG
- scripts: smoke·ledger·릴리스 에이전트·NAS 프리뷰 운영 스크립트

engine.public 로그 .bak과 apps/web/test-results 산출물은 커밋에서 제외했다.
This commit is contained in:
Yun Chan 2026-08-08 01:30:53 +09:00
parent 93dd8f82d7
commit 16e791e044
390 changed files with 243188 additions and 499 deletions

View file

@ -0,0 +1,114 @@
# 공개 DB 복구 리허설과 전환 게이트 — 2026-08-07
## 현재 상태
- 2026-08-07 18:26 KST owner 승인에 따라 공개 DB를 복구 named volume으로 전환했다. 공개 API
`api-vignette.chanpaca.net`은 이 Windows 호스트의 `127.0.0.1:8001`로 들어오며,
`vignette-dev-db:55432``vignette_recovered_prod_20260807`을 사용한다.
- 18:30 KST 공개 경로 재검증 결과 owner 기준 `app_user=84`, `sessions=30`, `turns=705`,
Google 계정 16, Google 소유 회기 30, orphan session 0이다. local/public health는 모두
`environment=prod`, `db=true`, `engine=true`, `engine_mode=claude_cli`이고 OpenAPI path는 119개다.
- `/auth/config`의 Google OAuth는 configured, dev login은 disabled이며, 미인증 `/auth/me`는 401,
Google login start는 `accounts.google.com`으로 302다. 공개 로그인 화면도 실제 브라우저에서 렌더됐다.
- 복구 후보에는 유효한 `auth_session`을 복사하지 않았으므로 모든 사용자는 Google 재로그인이 필요하다.
계정·회기 원장은 복구됐지만 기존 로그인 쿠키가 계속 유효하다는 의미는 아니다.
- 전환 직전 DB는 `vignette-dev-db-pre-recovery-20260807-182642`로 중지 보존했다. 자동 rollback을
검증한 앞선 실패 후보 2개도 중지 보존했으며, 어떤 원본 container/volume도 삭제하지 않았다.
- `VignettePublicRuntimeWatchdog`를 다시 활성화했고 18:30 KST check-only에서 engine, API, web,
cloudflared, public API가 모두 healthy였다.
- active recovered DB와 cutover 스크립트는 `restart=unless-stopped`를 사용해 Docker 재기동 뒤에도
같은 named volume을 자동 재연결한다. 이 변경은 실행 중 container를 재시작하지 않고 적용했다.
- 전환 전 DB는 2026-08-06 20:36 KST에 만들어진 익명 PGDATA였다. owner 기준
`app_user=706`, `sessions=448`, `turns=204`이지만, 대부분은 개발/E2E fixture였다.
- RLS context 없이 app role로 조회한 `sessions=0`, `turns=0`은 삭제 증거가 아니다. owner 조회에는
행이 있고 `pg_stat_user_tables.n_tup_del=0`, 보존 로그의 대상 테이블 DELETE/TRUNCATE/DROP도 0건이다.
- 운영에 가장 가까운 분리 원본은 detached Docker volume `08413cd7…cceba8`이다. 원본은 읽기 전용으로
조사했으며 `app_user=84`, Google account 16, Google-owned sessions 29, turns 705,
feedback 2,895가 있다. 마지막 사용자 갱신 흔적은 새 DB 생성보다 약 14시간 40분 앞선
2026-08-06 05:55 KST다.
- 현재 DB에는 이 원본에 없는 Google 회기 1개가 2026-08-07 07:55 KST에 생성돼 있다. 테스트라고
단정하지 않고 current-only delta로 보존한다. 이 회기는 종료 상태이며 턴 0, Alliance Pulse 1,
measurement 9, self-assessment 1, state 1, summary 1, model run 2를 가진다.
## 복구 가능성 증거
- 사고 전 후보 dump:
`D:\workspace\vignette-backups\vignette-before-test-data-cleanup-20260715-095617.dump`
- 현재 공개 대상 보존 dump:
`D:\workspace\vignette-recovery\current-public-target-20260807-161953.dump`
- SHA-256 `ceaf52230a8cd5dfc408c27728c894ca7290cc47d7e9a9d82e0e18e7239db145`
- 옛 원본 보존 dump:
`D:\workspace\vignette-recovery\recovered-original-20260807-161953.dump`
- SHA-256 `9e5f2185f146a31a322dbfc18b2fa1c3bfef64e951d58b8ff8d4aef0dc26a94a`
- 격리 복구 후보:
- container `vignette-recovered-prod-candidate`
- named volume `vignette_recovered_prod_20260807`
- localhost `55433`
- detached 원본과 current-only Google 회기 1개를 transaction merge
- migrations 01~16 적용, 최신 07·16 `ON_ERROR_STOP` 단일 transaction 재적용 통과
- owner 무결성: users 84, sessions 30, turns 705, Google sessions 30
- orphan·external ID 중복·case/session_no 중복·시간 역전 0
- 같은 source 재실행 `already-applied`, 추가 삽입 0
- learner RLS simulation: 기대 session 6, app role 관측 session 6
- post-merge dump:
`D:\workspace\vignette-recovery\candidate-post-delta-20260807-172059.dump`
- SHA-256 `d24eaa753526dff77d889ed2a48477d4df4bc2b854878543f89f4db6f6502e32`
- 비식별 merge manifest:
`docs/ops/evidence/recovery-delta-merge-manifest-2026-08-07.json`
## 백업과 복원 리허설
- `scripts/backup-vignette-db.ps1`은 running container와 readiness를 먼저 확인하고, custom-format dump를
container 내부에서 `pg_restore --list`로 검증한 뒤에만 `.partial`을 최종 dump로 원자 이동한다.
- 게시 manifest는 container ID, dump 크기, SHA-256, 검증 방식을 포함하고 DB 본문·비밀은 포함하지 않는다.
- 2026-08-07 현재 public-target 백업:
`D:\workspace\vignette-backups\vignette-dev-db-vignette-20260807-080647Z.dump`
- SHA-256 `c39fb23ad4845275ccf1fbd659a18bc57da768d70fa16ad8ed917551bc089cf3`
- 외부 포트 없는 일회용 DB에 `pg_restore --exit-on-error` 성공
- 129 tables, 9,646 rows, users 706, sessions 448, turns 204
- session→user/turn→session orphan 0, invalid index 0
- temporary container/volume cleanup 뒤 잔여 0
- 전환 직전 public-target 보존 dump:
`D:\workspace\vignette-backups\vignette-dev-db-vignette-20260807-091057Z.dump`
- SHA-256 `6b84d5c8fd82edd1c40f407e4b5d28e5c32ea6c00c953aaeb2ff5551f4e8af1f`
- engine 설정을 현재 공개 런타임과 맞춘 최종 복구 DB 보존 dump:
`D:\workspace\vignette-recovery\vignette-recovered-prod-candidate-vignette-20260807-092630Z.dump`
- SHA-256 `d7bcc396972482ea7691b3d25f923f92bb4e05c3a54fdc7d50954144a48868ed`
- 공개 전환 후 active recovered DB 보존 dump:
`D:\workspace\vignette-backups\vignette-dev-db-vignette-20260807-095511Z.dump`
- SHA-256 `660695c9b3590198de35d6027b70b2df8ffb5a684ccd1572415b8b2a3865bb88`
- custom archive TOC 검증과 원자 게시 manifest 생성 완료
- 예약 실행, 실패 알림, NAS/off-host 암호화 복제, restore drill 주기와 보존 정책은 별도 운영 게이트다.
## 확인된 재발 메커니즘과 패치
기존 `scripts/dev-up.ps1``vignette-dev-db`가 실행 중이 아니면 컨테이너를 제거하고 PGDATA mount 없는
새 컨테이너를 만들었다. 기존 익명 volume은 삭제되지 않지만 서비스가 새 빈 volume을 보게 되는 형태가
현재 결과와 일치한다. 보존된 Docker event가 없어 실행 actor를 단정하지는 않는다.
재발 방지 패치는 다음 계약으로 바꿨다.
1. 정지된 기존 DB는 제거하지 않고 `docker start`를 우선한다.
2. 새 DB는 고정 named volume `vignette-dev-db-pgdata`를 명시한다.
3. 일반 `dev-up`에는 container/volume 삭제 경로가 없다.
4. 시작·readiness 실패 시 container와 volume을 보존하고 fail closed한다.
5. focused pytest 5 passed, Ruff, Windows PowerShell 5.1 AST parser, diff check를 통과했다.
## 전환 결과와 남은 확인
- [x] current-only Google 회기 1개와 직접 provenance 행을 격리 후보에 UUID remap하여 transaction merge한다.
기존 후보 계정/profile은 덮어쓰지 않고 auth session은 복사하지 않았다.
- [x] 병합 후보의 계정·회기·턴·orphan·RLS·constraint·멱등 replay를 다시 고정한다.
- [x] post-merge dump의 disposable clone에서 G4/G5 DB-backed 브라우저 폐루프를 확인한다.
desktop 2/2·mobile 2/2, route mock 0, G4/G5 실제 POST·read-model reload·멱등 재확인,
UUID 노출·마이크 호출·중복 원장 0을 확인했다. 증거 SHA는 `7c2336bf…7cc93`이다.
- [x] 윤찬의 공개 DB 전환 승인을 받는다.
- [x] 전환 직전 현재 public-target dump SHA를 다시 확인한다.
- [x] watchdog을 짧게 정지하고 현재 container를 보존 이름으로 바꾼 뒤, recovered named volume을
`vignette-dev-db:55432`에 연결한다.
- [x] API 8001 재기동 후 public health/OpenAPI/auth/OAuth start를 확인한다.
- [ ] 실제 사용자가 Google 재로그인한 뒤 본인 계정과 과거 소유 회기 목록이 보이는지 확인한다.
이 브라우저에는 기존 로그인 상태가 없어 계정 가장 없이 사용자 확인으로 남겼다.
- [x] 실패 시 기존 container로 되돌리는 자동 rollback 경로를 두 차례 실증했다. 최종 전환은 통과했다.
공개 전환 뒤에도 원본·전환 직전 container와 모든 dump/volume은 사용자 확인과 안정화가 끝날 때까지 삭제하지 않는다.