vignette/docs/ops/handoff-goal-production-2026-08-29.md

350 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Vignette 전체 개선 목표 · 정식 배포 핸드오프
작성 시각: 2026-08-29 21:18 KST
작업 루트: `D:\workspace\vignette`
Goal ID: `01a04217-f73b-7303-b597-401fa7f5d290`
Goal: `엑셀 파일의 모든 내용을 마친다`
## 0. 2026-08-30 재개 후 현행 상태
이 절이 아래 2026-08-29 종료 스냅샷보다 우선한다. 현재 이어받기 Goal ID는
`01a04dd0-93ef-7d02-a9cb-40682fd0988a`다.
- `preserved_total_size_bytes=52,973`과 decode 3/90, 현재 DB 참조 decode 2/6을 initializer→manifest v3→
bootstrap/cutover/task-recovery/final receipt→API health까지 결속했다.
- manifest 이후 생성된 UUID형 아바타도 URL 기록·현재 DB 참조·GET/HEAD에서 3MB 제한, Pillow full decode,
확장자-format 일치를 다시 검사한다. 검증한 동일 bytes를 응답해 검사 뒤 재오픈 경쟁을 없앴고, 사후 손상은
current invalid/health fallback과 public 404로 닫는다. 원본 bytes와 DB URL은 삭제하지 않았다.
- API 전체 `1074 passed / 1 skipped`, gateway `68 passed`, runtime/bootstrap 통합 `154 tests OK`, web typecheck/build,
이미지+사람 게이트 route E2E가 통과했다. SSOT checker와 scoped diff check도 통과했다.
- Codex 내장 브라우저의 격리 local stateful fixture에서 8.5초 뒤 overlay 0·heading 정상, content
`keep_quarantine` effect 0, 증거 없는 release 승인 disabled→`reject` effect 0, 증거 4종 promote 승인
lifecycle effect 정확히 1을 확인했다. 이는 실DB/public proof를 대신하지 않는다.
- clean 통합 브랜치는 `YunChan/goal-production-20260830`이며 UI 공통화 두 커밋 위에 runtime·usage·G8·auth·
관리자 UI·온보딩 계정 전환을 좁은 커밋으로 결합했다. push·Pages 배포·public runtime/task 변경은 아직 없다.
- 풀스택 `layout-visual-gate` 15/15와 `session-layout` 8/8은 Docker Desktop이 꺼져 있고 활성 public watchdog이
daemon 기동 즉시 기존 runtime 복구를 시도할 수 있어 승인 대기다. 승인 시 watchdog을 일시 중지·비활성화하고
고유 DB/container/volume과 56432/58000/55173만 사용한 뒤 exact cleanup, Docker 종료, watchdog 원상복구를 수행한다.
- 최종 외부 게이트는 여전히 C001 적격 외부 임상 검수와 REQ-008 실제 Windows 재부팅 smoke다. 둘 다 추정 증거로
닫지 않는다.
## 1. 이전 세션 종료 결정과 당시 결론
사용자가 세션 장기화를 이유로 상세 핸드오프 후 익일 재개를 지시했다. 21:15 KST부터 모든 에이전트의 새 편집을 중단했고, 로컬 stage·commit·push·Cloudflare Pages 배포·라이브 DB 변경·API/tunnel 재시작·예약 작업 변경·PC 재부팅은 수행하지 않았다.
당시 소스는 **배포 가능 GREEN이 아니었다**. 특히 종료 직전 실제 이미지 디코딩 감사를 추가로 수행한 결과, 연결된 운영 DB가 참조하는 아바타 8개 중 6개가 깨진 동일 PNG payload라는 사실을 확인했다. 파일 존재와 해시만 보존하면 사용자가 신고한 깨진 이미지가 그대로 남았다. 이 결함은 위 2026-08-30 후보에서 public 404/fallback으로 닫았지만 아직 정식 배포 전이다.
Goal은 완료 처리하지 않았다. 기술 구현·정식 배포·실제 재부팅 증명과 별개로 C001 적격 외부 임상 검수도 여전히 인간 게이트다.
## 2. 완료된 엑셀 산출물
- 산출물: `D:\workspace\vignette\outputs\01a04217-f73b-7303-b597-401fa7f5d290\Vignette_개선관리_완료.xlsx`
- SHA256: `c832547f30ae0664e54b302c8e9f62cc157f31022ad158d17803d8cac988d8bd`
- 크기: 2,751,365 bytes
- 검증: 6 sheets, 11 requirements, 38 formulas, formula error 0, inspection files 18
- 상태 집계: 완료 9, 검토 2, 보류 0
- 남은 두 검토 항목:
- C001: 적격 외부 임상 검수 입력과 서명 증빙
- REQ-008: 실제 PC 재부팅 뒤 예약 작업 기반 자동복구와 공개 smoke
- C001 셀 상태: `G23=pending-valid`, `G24=PACKAGE_MATCH`, `B24=pending_external_review`, `J3=검토`; 외부 검수 입력 46칸은 의도적으로 비워 두었다.
- 임상 검수 내용을 추정하거나 가짜로 작성하면 안 된다.
## 3. UI 작업 상태
깨끗한 UI 후보 워크트리는 아래와 같다.
- 경로: `D:\workspace\vignette-ui-image-release-20260829`
- 브랜치: `YunChan/ui-image-resilience-release-20260829`
- HEAD: `a73b9efff77e3c575e32976bbe4f1ed404e103f0`
- 관련 커밋:
- `35a62fda` 탭 구조와 이미지 복구를 공통화
- `a73b9eff` 분석 탭과 축어록 계층을 정돈
- 워크트리 상태: clean
- 로컬 미리보기: `http://127.0.0.1:5188`
반영된 브라우저 코멘트:
- 학습 대시보드의 불필요한 안쪽 컨테이너 스타일 정리
- 교수자 요약 카드 상단 간격 분리
- 관리자 사용자 탭의 의미 없는 외곽 컨테이너 제거 및 공통 탭 컴포넌트화
- Topbar 프로필 이미지 실패 시 깨진 이미지 아이콘 대신 안전한 fallback 표시
- 학습자 상세 분석의 4개 탭이 한 줄을 유지하도록 수정
- 회기 축어록 내담자 발화의 불필요한 테두리 제거
- Google 로그인 단일 진입 UX 정리
이 UI는 로컬 내장 브라우저에서 시각 확인했지만 production에는 배포하지 않았다. 당시 내장 브라우저에는 로컬 분석/축어록 탭과 production 관리자 탭이 열려 있었다. 익일에는 탭 존재를 가정하지 말고 새로 열어 확인한다.
## 4. 운영 데이터와 업로드 보존 감사
### 4.1 정확한 소스 경계
아래 세 root의 `profile-avatars`만 source allowlist로 사용했다.
1. `D:\workspace\vignette\apps\api\uploads`
2. `D:\workspace\vignette-public-runtime-bf5f7352\apps\api\uploads`
3. `D:\workspace\vignette-public-runtime-dba9b75a3887\apps\api\uploads`
결과:
- union object count: 93
- union inventory SHA256: `9d703126f78d4fc8330408835d76a7d680276240dc578d6fc9ca420c2f25e6aa`
- union total bytes: 52,973
- maximum object bytes: 28,208
- same-name content conflict: 0
- invalid/nested/reparse entry: 0
- strict server-generated UUID-token filename shape: 93/93
- DB references found in union: 8/8
연결 DB의 개인정보 없는 결속값:
- database target SHA256: `81fe4a2844b7340f21396931fa18580e24358f857a08cc60540ddf8a4f8789b5`
- reference count: 8
- unique referenced objects: 8
- reference-set SHA256: `70926cf36ceb2375dd6c471bc59a38138460d8e4895ad1f2bddbcf1a49210d2b`
- active private multimodal audio: 0
### 4.2 종료 직전 발견한 손상 이미지
Pillow 12.2.0의 실제 decode/verify와 확장자-format 일치를 파일명·경로·사용자 ID·이메일·URL을 출력하지 않고 검사했다.
- 전체 93개: 정상 decode 3, 실패 90
- 정상 3개: JPEG 2개, PNG 1개; 크기 225×225, 512×512, 1×1
- 운영 DB 참조 8개: 정상 2, 실패 6, missing 0
- 정상 참조 2개: JPEG, 225×225 및 512×512
- 실패 참조 6개: 모두 70 bytes, PNG signature는 있으나 full decode 실패
- 실패 6개는 동일한 content 한 종류다.
근거 파일: `docs/ops/evidence/avatar-decode-audit-2026-08-29.json`
이 결과의 의미:
- “93개를 덮어쓰기 없이 복사했다”만으로는 깨진 이미지 문제가 해결되지 않는다.
- 6개 손상 payload와 해당 DB reference를 승인 없이 삭제·초기화하면 안 된다.
- 원본을 찾을 수 있으면 복구하고, 찾을 수 없으면 손상 bytes는 private forensic 보존하되 public static 응답은 404/fallback으로 보내는 정책이 권장된다.
- 현재 UI 후보의 `ResilientImage`가 시각적 fallback은 제공하지만, backend가 손상 파일을 정상 이미지처럼 공개하는 문제와 데이터 복구 정책은 별도로 닫아야 한다.
- 익일 첫 결정 게이트는 다음 둘 중 하나다.
1. 권장: 손상 bytes와 DB reference를 보존하고, manifest에 decode 상태를 결속해 손상 객체는 public serve하지 않으며 UI fallback을 사용한다. 이후 원본 복구 또는 소유자 승인 기반 정리를 별도 수행한다.
2. 엄격: 6개 원본을 복구할 때까지 API cutover 자체를 fail-closed로 막는다.
## 5. 업로드·DB·재부팅 복구 코드 상태
2026-08-29 dirty master에서 시작한 다음 안전 계약은 현재 clean 통합 브랜치에 좁은 커밋으로 결합돼 있다.
- exact 3-root union + caller-pinned count/inventory digest
- source copy 전후 재스캔과 create-only copy
- DB reference 8/8 보존 확인
- manifest v3의 privacy-safe path/content hash, total bytes, decode 상태와 DB target binding
- 새 API가 자기 pool의 repeatable-read snapshot으로 DB target과 현재 avatar refs를 DDL 전에 검증
- 모든 신규 physical DB connection이 target digest를 재검증
- production Uvicorn `--workers 1` 고정
- upload/PATCH/onboarding write lease와 freeze drain
- unrelated profile PATCH가 stale avatar URL을 되살리지 못하도록 수정
- 신규 업로드는 UUID(user id)+random token 이름, create-only hard-link publish
- static 공개 범위는 decode-valid manifest-preserved path 또는 full decode를 재통과한 strict runtime-generated filename으로 제한
- preserved bytes immutable memory cache와 신규 업로드 single-read response로 per-request 전체 hash DoS와 disk reopen TOCTOU 제거
- boot/watchdog task를 새 정의로 disabled 설치 → exact action 계약 확인 → 둘을 함께 enable
- 두 번째 task 설치/enable 실패 시 두 task 모두 disabled로 보상
- exact root task path `\` 결속
재개 후 위 미완료 연결은 해소했다. initializer manifest/result와 bootstrap의 cutover/task-recovery/final passed receipt가
모두 `preserved_total_size_bytes=52,973`과 decode proof를 교차 검증한다. 남은 것은 승인된 격리 풀스택 E2E와 정식
배포·public browser proof이지 manifest 생산자/소비자 계약 불일치가 아니다.
## 6. 마지막 검증 결과
2026-08-30 재개 후 보고:
- 전체 API: `1074 passed / 1 skipped`
- gateway: `68 passed`
- runtime/bootstrap 결합: `154 tests OK`
- API runtime focused: `31 passed`
- web typecheck/build: PASS
- 이미지 복구+사람 게이트 focused browser E2E: PASS
- Codex 내장 브라우저 local stateful 사람 게이트: overlay 0, 보류/반려 effect 0, 승인 effect 1
- SSOT checker와 scoped `git diff --check`: PASS
주의:
- full API·web build/typecheck·route/internal-browser proof는 현재 후보 기준이다.
- 실제 Postgres를 쓰는 `layout-visual-gate` 15/15와 `session-layout` 8/8, production browser proof는 아직 없다.
- Python 3.12/3.14의 `tempfile.TemporaryDirectory`가 현재 sandbox ACL과 충돌해 생성 직후 접근 거부를 냈다. 동일 테스트는 Python 3.11에서 정상 통과했다. 익일 테스트는 `py -3.11` 또는 `C:\Users\encep\AppData\Local\Programs\Python\Python311\python.exe`를 사용한다.
- `D:\workspace\vignette\tmp` 아래 접근 거부 임시 디렉터리들은 테스트 환경 잔재다. 광범위 재귀 삭제하지 말고, 필요 시 exact path와 ACL을 확인한 뒤 별도로 정리한다.
## 7. 이전 세션 Git·워크트리 기준선
2026-08-29 21:15 KST 기준:
- shared checkout: `D:\workspace\vignette`
- branch: `master`
- HEAD: `ac9b7026881139780938f4c4f2b89a235b0a0c08`
- HEAD tree: `b07cb4dd6b9b33b650b59b24fc1bc4b8bf2b48f8`
- `origin/master`보다 20 commits ahead
- shared checkout은 사용자 작업과 이번 작업이 섞인 큰 dirty tree다. `git status --untracked-files=all`은 접근 거부 tmp를 포함해 547 entries를 셌다.
- `git add .`, `git commit -a`, whole-tree copy는 금지한다.
관련 worktree:
- UI clean candidate: `D:\workspace\vignette-ui-image-release-20260829`, `a73b9eff`
- old runtime-storage candidate: `D:\workspace\vignette-runtime-storage-release-20260829`, `dba9b75a`, dirty; 현행 source of truth로 사용하지 않는다.
- current public runtime: `D:\workspace\vignette-public-runtime-dba9b75a3887`, detached `dba9b75a`
- current scheduled tasks `VignettePublicRuntime`, `VignettePublicRuntimeWatchdog`는 마지막 확인 시 enabled/Ready이며 여전히 old `dba9b75a` runtime을 가리킨다.
- current Pages production은 deployment `0c60261e`, source `5bf89ff`였다. UI 후보는 아직 미배포다.
## 8. 현재 변경 파일 경계
API 소유 범위:
- `apps/api/app/config.py`
- `apps/api/app/db.py`
- `apps/api/app/main.py`
- `apps/api/app/routes/users.py`
- `apps/api/app/upload_runtime.py`
- `apps/api/app/upload_storage.py`
- `apps/api/app/test_upload_storage_contract.py`
- `apps/api/app/test_engine_health_contract.py`
- `scripts/validate-public-runtime-upload-manifest.py`
runtime/bootstrap 핵심 범위:
- `scripts/initialize-public-runtime-upload-root.py`
- `scripts/initialize-public-runtime-upload-root.ps1`
- `scripts/bootstrap-legacy-public-runtime-upload-root.ps1`
- `scripts/validate-public-runtime-offline-quiescence.py`
- `scripts/probe-public-runtime-database-identity.py`
- `scripts/probe-public-runtime-upload-root.py`
- `scripts/public_runtime_database_identity.py`
- `scripts/public-runtime-upload-root.ps1`
- `scripts/public-runtime-task-maintenance.ps1`
- `scripts/public-runtime-task-definition-cutover.ps1`
- `scripts/start-public-runtime.ps1`
- `scripts/boot-public-runtime.ps1`
- `scripts/watch-public-runtime.ps1`
- `scripts/install-public-runtime-task.ps1`
- `scripts/register-boot-task.ps1`
- 관련 focused tests 10개
- `docs/ops/public-runtime-watchdog.md`
- 관련 architecture/local-development/testing 가이드와 `docs/dev_dashboard.html`
마지막 직접 수정된 task tests:
- `scripts/test_public_runtime_upload_root.py`
- `scripts/test_public_runtime_task_definition_cutover.py`
파일 전체를 자동 stage하지 말고 각 diff에 선행 사용자 변경이 섞였는지 다시 확인한다.
## 9. 익일 재개 순서
### 9.1 현재 truth 재확인
1. Windows/PowerShell 판, 현재 경로, Git HEAD/worktree/status를 다시 확인한다.
2. 이 문서와 `docs/dev_dashboard.html`, `docs/ops/backlog-2026-06-26.md`, `docs/ops/public-runtime-watchdog.md`를 읽는다.
3. production/API/task state는 문서만 믿지 말고 read-only로 다시 확인한다.
4. 세 source root union을 다시 계산해 `93 / 9d7031... / 52,973 bytes`, DB `8 refs / 70926c... / private audio 0`과 일치하는지 확인한다.
5. 아바타 decode audit도 재실행해 `DB refs valid 2 / invalid 6 / missing 0`이 유지되는지 확인한다.
### 9.2 코드 blocker 해소
1. `PreservedInventory.total_size_bytes`를 initializer privacy-safe result, manifest, bootstrap cutover receipt, task-recovery receipt, final passed receipt까지 끝까지 결속한다.
2. exact expected total `52,973`을 CLI 인자와 tests에서 pin한다. count 93만으로 same-count substitution을 허용하지 않는다.
3. 6개 손상 DB-ref에 대한 정책을 소유자와 결정한다. 어떤 경우에도 원본 bytes/DB reference를 승인 없이 삭제하지 않는다.
4. 권장 정책을 택하면 decode-valid preserved object만 immutable public cache로 제공하고, invalid object는 private forensic 보존 + public 404/fallback 처리하며 privacy-safe 손상 count를 health/receipt에 기록한다.
5. 새 stable upload root가 비어 있거나 exact expected set임을 cutover 전후에 증명한다. UUID형 pre-existing extra를 무조건 허용하지 않는다.
6. 문서·SSOT·얇은 backlog를 실제 계약과 일치시킨다.
### 9.3 통합 테스트
Python 3.11로 최소 아래를 한 번에 다시 실행한다.
```powershell
py -3.11 -B -X utf8 -m unittest `
apps.api.app.test_upload_storage_contract `
apps.api.app.test_engine_health_contract `
scripts.test_initialize_public_runtime_upload_root `
scripts.test_legacy_public_runtime_upload_bootstrap `
scripts.test_public_runtime_environment_handoff `
scripts.test_public_runtime_listener_pid_probe `
scripts.test_public_runtime_task_definition_cutover `
scripts.test_public_runtime_task_maintenance `
scripts.test_public_runtime_upload_release_safety `
scripts.test_public_runtime_upload_root `
scripts.test_public_runtime_watchdog_provenance `
scripts.test_start_public_runtime_contract -v
```
추가 검증:
- Windows PowerShell 5.1 AST parse for all changed `.ps1`
- Python compile for all new/changed `.py`
- scoped `git diff --check`
- API의 전체 관련 test suite
- 깨끗한 통합 후보에서 `apps/web``npm run typecheck`, `npm run build`
- `e2e/layout-visual-gate.spec.ts` 15/15
- `e2e/session-layout.spec.ts` 8/8
- `e2e/image-resilience.spec.ts`
- `e2e/tabs-behavior.spec.ts`
- Google auth/onboarding/admin/profile avatar 실제 브라우저 E2E
### 9.4 좁은 커밋과 clean candidate
1. shared dirty master에서 이번 runtime 파일만 line-by-line 검토해 좁게 stage한다.
2. author는 `Yun Chan <yunchan@twentyoz.kr>`, 한글의 짧은 커밋 메시지를 사용한다.
3. `git add .` 금지.
4. master `ac9b7026` 이후 runtime commit을 만들고, 새 clean release worktree/branch를 만든다.
5. UI 커밋 `35a62fda`, `a73b9eff`를 순서대로 cherry-pick한다.
6. clean candidate SHA/tree, clean status, 테스트 결과를 고정한다.
### 9.5 사용자 승인 후에만 정식 전환
후보가 GREEN일 때 사용자에게 아래 범위를 정확히 제시하고 승인받는다.
> 후보 커밋 `<sha>`를 원격에 push하고 Cloudflare Pages production과 이 PC의 public API/tunnel·두 예약 작업을 새 detached runtime으로 전환해도 돼? 기존 API/tunnel은 약 1분 재시작되고, 검증된 upload inventory는 덮어쓰기 없이 새 영구 root에 보존돼.
승인 전 금지:
- `git push`
- Cloudflare Pages production deploy
- public API/cloudflared stop/restart
- scheduled task reinstall/retarget/enable 변경
- DB avatar URL 수정
- stable upload root 생성/복사
승인 후에도 Pages 자산 보존은 stale default script를 그대로 쓰지 않는다. 최소 다음 실제 production 세대의 immutable asset graph를 explicit origins로 보존한다.
- `https://0c60261e.vignette-b1q.pages.dev`
- `https://ef48c0ae.vignette-b1q.pages.dev`
- `https://1f1ddf18.vignette-b1q.pages.dev`
배포 뒤에는 HTTP 200만 보지 않는다. custom domain의 신규 index/asset hash·MIME·신규 UI marker·구버전 marker 부재, API health manifest/DB/freeze proof, Google 로그인, super account role/onboarding, 데이터/아바타 fallback을 Codex 내장 브라우저로 보여준다.
### 9.6 실제 재부팅 게이트
production 전환과 browser smoke가 끝난 뒤에만 아래 문구로 명시 승인받는다.
> 지금 이 PC를 재부팅해도 돼. 저장하지 않은 작업은 없고, encep 계정으로 로그인한 뒤 REQ-008 자동복구 smoke까지 계속 진행해.
재부팅 뒤에는 먼저 task를 수동 실행하지 않는다. 로그인 후 자동으로 API/tunnel/tasks가 복구되는지 관찰하고, 공개 health·Google 로그인·데이터·이미지·새 runtime SHA/task action을 증명한다.
## 10. C001 외부 임상 검수 게이트
C001은 코드·UI·운영 배포로 대신할 수 없다. 적격 검수자의 실제 입력, 자격/역할, 검토 시각, 대상 버전/패키지 결속, 승인 또는 수정 요청을 받아 workbook의 지정 셀에 반영해야 한다. 검수자가 없으면 최종 Goal은 `기술 완료 / 외부 검수 대기`로 정확히 남긴다.
## 11. 절대 하지 말 것
- 손상 아바타 6개의 DB URL이나 파일을 승인 없이 삭제·초기화하지 않는다.
- 90개 decode-invalid legacy payload를 정상 이미지로 간주하지 않는다.
- 한 개 source root만 복사해 8개 DB ref를 복구했다고 주장하지 않는다.
- old runtime root 2개만 보고 데이터가 온전하다고 판단하지 않는다.
- general `/uploads` directory를 static mount하지 않는다.
- production Uvicorn worker를 2개 이상 띄우지 않는다.
- reset receipt 없이 initial nonzero refs → current zero를 정상으로 받아들이지 않는다.
- 예약 작업을 새 정의로 교체한 뒤 검증 전에 enable하지 않는다.
- 실제 재부팅 전 task를 수동 실행해 자동복구 증거를 오염시키지 않는다.
- 외부 임상 검수 내용을 만들어내지 않는다.
- dirty tree에서 전체 stage/commit/copy하지 않는다.
- Python 3.12/3.14 tempfile ACL 오류를 제품 테스트 실패와 혼동해 같은 방식으로 반복하지 않는다.
## 12. 재개 프롬프트
다음 세션에서 아래처럼 시작하면 된다.
> `docs/ops/handoff-goal-production-2026-08-29.md`를 먼저 읽고, live/Git/DB/avatar decode truth를 read-only로 재검증해. 손상 DB-ref 6개의 보존·fallback 정책과 `preserved_total_size_bytes=52973` end-to-end 결속부터 마무리하고, 전체 통합 GREEN 전에는 stage/push/deploy/runtime/task/DB를 건드리지 마. clean candidate가 준비되면 SHA와 승인 범위를 먼저 보여줘.