| Web typecheck | npm run typecheck | Passed |
| Design SSOT / auth visual | npm run check:design-ssot / npx playwright test e2e/auth-visual.spec.ts --project=chromium-single-run --reporter=line / npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run --reporter=line | SSOT checker passed; login/onboarding light-dark desktop-mobile 1 passed; 14 core screens × 7 widths visual gate 14 passed. |
- | Full Playwright E2E baseline | npm run e2e:parallel / npm run e2e:single-run / npm run e2e:list | 2026-08-28 현재 수집은 1138 tests / 62 files다. 이 숫자는 수집량이며 현 작업트리 전체 GREEN과 동일하지 않다. G8 clean-head release gate는 candidate 112/112와 실제 NAS-origin 112/112를 통과했다. 이전 단일 120/120과 2026-07-15의 fixture desktop/mobile 166/166 + DB/engine/provider 직렬 49/49 = 215/215는 범위가 다른 역사 기준선으로 보존한다. |
+ | Full Playwright E2E baseline | npm run e2e:parallel / npm run e2e:single-run / npm run e2e:list | 2026-08-29 현재 수집은 1158 tests / 64 files다. 이 숫자는 수집량이며 현 작업트리 전체 GREEN과 동일하지 않다. G8 clean-head release gate는 candidate 112/112와 실제 NAS-origin 112/112를 통과했다. 이전 단일 120/120과 2026-07-15의 fixture desktop/mobile 166/166 + DB/engine/provider 직렬 49/49 = 215/215는 범위가 다른 역사 기준선으로 보존한다. |
| Refactor governance P1~P8 | ruff check app / pytest -q app / pytest -q engine_gateway / npm run typecheck / npm run check:api-types / npm run check:design-ssot / npm run check:dead-code / npm run check:duplication / npm run build / npm audit --audit-level=high / full Playwright | Backend 400 passed, gateway 29 passed, web gates/build/audit passed, vulnerabilities 0, production duplication 1 clone/15 lines/0.03%, Playwright 215/215 passed. 상세 근거는 ops/refactor-governance-2026-07-15.md. |
| API typegen SSOT | npm run check:api-types | Passed; FastAPI OpenAPI → src/lib/api.gen.ts stale check |
| Outcome & Alliance OS G0 | py -3.11 -X utf8 -m pytest -p no:cacheprovider apps/api/app/test_measurement_contract.py apps/api/app/test_runtime_schema_ssot.py -q / scripts/check-measurement-ledger.sql / measurement·API contract checks / web typecheck / DB-backed session-persistence focused E2E 3종 | G0 contract/schema 11 passed, 기존 backend 100 passed, auth 39 passed. Python→JSON Schema→TypeScript→PostgreSQL enum·필수필드 계약이 일치하고 8개 deterministic benchmark가 검증됐다. Live PostgreSQL에서 learner/client/evaluator 가시 행 1/1/2, 교차 누수 0, append-only guard 2를 확인했다. 학습자 턴→교수자 대시보드, 워크시트 검수, 종료 deep 평가→durable 리뷰 E2E는 각각 1 passed. G0/AOS-001~004 완료. |
@@ -1119,11 +1129,11 @@
| Outcome & Alliance OS G4/G5 caller | pytest test_session_learning_producer.py test_deliberate_practice.py test_deliberate_practice_store.py test_calibration_transfer.py test_calibration_transfer_store.py / scripts/smoke-session-learning-producer.py | focused 100 passed, 전체 API 855 passed. 실제 종료 회기·durable turn 2개·ready 평가에서 production caller가 G4 처방 원장 5종을 각 1개 생성하고 deterministic replay한다. G5는 prediction lock 전 observation 0, lock callback 뒤 failed 독립 관찰과 evaluator model-run 각 1개이며 replay 중복 0이다. learner/teacher projection이 일치하고 mastery/pass/assessment/transfer 자동 승격은 전부 0건이다. 합성 교육 fixture이며 임상·실제 숙달 주장이 아니다. |
| 학생 자기주도 전체 루프 UI | npx playwright test e2e/self-directed-learning-loop.spec.ts --project=chromium-desktop --project=chromium-mobile --workers=1 --reporter=dot / npx playwright test e2e/alliance-pulse.spec.ts --project=chromium-desktop --project=chromium-mobile --workers=2 / npm run typecheck / 고정 캡처 직접 QA | 실제 src 홈 추천→목표 선택→텍스트/SSE 회기→종료·리뷰→G4 처방 키보드 CTA→새 회기 재연습 시작이 desktop/mobile 2/2, typecheck를 통과했다. 페이지 overflow 0, 홈 CTA 44px·4.5:1 이상 대비, typed launch intent와 replay 모드 보존, 내부 criterion/counterevidence/UUID 비노출을 확인했다. 390×844·320×568 활성 회기는 아바타/문구 겹침 0과 축어록/입력창 내부 포함을 통과했다. 320px 리뷰 1~5 척도는 다섯 선택지 44px 이상·내부/페이지 overflow 0이고 전체 desktop/mobile 8/8이다. 모든 미소유 API는 fixture 404이며 실제 로그인/API/DB 증거가 아니다. |
| Contract SSOT aggregate DTO | py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_evaluation_persistence.py app/test_evaluator_model_routing.py app/test_teacher_dashboard.py app/test_rbac_idor.py app/test_session_turn_persistence.py -q / npm run check:api-types / npm run typecheck / npm run build | 50 backend passed; learner sessions, session review/worksheet, teacher dashboard, session start/detail DTOs use generated ApiSchema aliases with UI fallback. Stage responses are OpenAPI enum unions, including review phase key/label, reached phase, evaluation summary/trigger responses, and teacher session/growth stage. .github/workflows/api-contract.yml runs npm run check:api-types on API/Web contract changes. |
- | 개선관리 C-002·C-003 / REQ-001~008 | pytest app/ -q / pytest engine_gateway/ -q / 항목별 focused pytest·DB/browser E2E / npm run check:api-types / npm run build | 내부 기술 DONE. 2026-08-28 최종 API 1002 passed, gateway 68 passed, Ruff·API type check·typecheck·lint·production build PASS. focused: C-002 3, C-003 11, REQ-001 auth 41 + callback access-log redaction 6 + OAuth UI desktop/mobile 4 + route-mock desktop/mobile 2 + 실제 관리자 DB/browser lifecycle 1, REQ-002·005 26(경고 1) + protocol 실제 DB lifecycle 1 + persona 실제 DB/browser lifecycle 1, REQ-003 34, REQ-004 직접 정책 85·관련 route/read-model 163·desktop/mobile 2, REQ-006 24, REQ-007 관련 163·live/session 50·DB E2E 1, REQ-008 health 1 passed. Google OIDC는 provider-verified 이메일을 도메인·사전등록 없이 learner·approved로 허용하고 suspended는 보존한다. 실제 관리자 E2E는 승인 전 403/대기→승인 후 동일 세션 200→피드백 OFF 영속 재조회→비활성화·세션 0을, persona E2E는 작성→승인→catalog→주호소→exact ID/version pin→controlled SSE→DB 2턴과 exact cleanup/설정 원복을 확인했다. 최종 레이아웃 재실행은 시각 게이트 15/15·포커스 106/106·세션 8/8, 실패·skip 0이며 격리 포트와 프로세스를 모두 종료했다. 전체 Playwright 현 작업트리 GREEN 수치로 환산하지 않는다. |
- | 개선관리 C-001 외부 임상 승인 | scripts/check-clinical-crisis-review.py / run-clinical-crisis-technical-observations.py / data/clinical/p1-crisis-review-cases.json / docs/ops/clinical-crisis-protocol-review-2026-08-27.md | 내부 기술 READY · 외부 GATE 미완료. v2 P1 합성 사례 6건과 상태별 canonical fail-closed 판정기를 유지한다. 생성·스트림 실제 경로 6/6, 런타임 패키지 8개 HEAD·작업트리 SHA 일치, focused 90 passed·Ruff PASS를 확인했다. 개선관리 완료본은 6번째 C-001 외부검토 시트에서 사례별 5필드 30개·청소년 답변 6개·검토자 자격 포함 승인 10필드·서명 증거를 받고 canonical checker와 패키지 해시가 모두 맞을 때만 완료로 전파한다. 현재 gate는 pending-valid지만 외부 결정 0건·review_complete=false라 10 완료/1 검토/총 11을 유지한다. |
+ | 개선관리 C-002·C-003 / REQ-001~008 | pytest app/ -q / pytest engine_gateway/ -q / 항목별 focused pytest·DB/browser E2E / npm run check:api-types / npm run build | 완료 9 · 검토 2. C-002·C-003과 REQ-001~007은 요구·기술 증거가 완료됐다. 2026-08-29 API 1074 passed/1 skipped, gateway 68 passed, typecheck와 G8 focused desktop/mobile 10/10이 PASS다. 격리 local Codex 내장 브라우저는 8.5초 overlay 0·heading 정상, content 보류와 증거 없는 release 반려 effect 0, 증거 4종 promote 승인 lifecycle effect 1을 확인했다. 이는 local stateful fixture GREEN이며 실DB/public proof가 아니다. REQ-001은 기존 Gmail 인증 세션의 관리자 권한·온보딩 비전환·복구 이력 가시성을 확인하고 소유자가 수락했다. REQ-008은 공개 P20 실제 응답과 복구 계약을 확인했지만 실제 Windows 재부팅 smoke가 없어 검토다. C-001은 별도 외부 GATE다. 전체 Playwright 현 작업트리 GREEN 수치로 환산하지 않는다. |
+ | 개선관리 C-001 외부 임상 승인 | scripts/check-clinical-crisis-review.py / run-clinical-crisis-technical-observations.py / data/clinical/p1-crisis-review-cases.json / docs/ops/clinical-crisis-protocol-review-2026-08-27.md | 내부 기술 READY · 외부 GATE 미완료. v2 P1 합성 사례 6건과 상태별 canonical fail-closed 판정기를 유지한다. 생성·스트림 실제 경로 6/6, 런타임 패키지 8개 HEAD·작업트리 SHA 일치, focused 90 passed·Ruff PASS를 확인했다. 현행 개선관리 완료본 SHA는 c832547f…d8bd이며, 이전 C-001 기술 사전검증 원장의 0a64d1bc…1177과 8/3 동기화본 aab6ef52…e26a은 역사 스냅샷으로 보존한다. 후속 원장은 6시트·11요구·38수식·오류 0과 현재 package match를 기록한다. 6번째 C-001 외부검토 시트에서 사례별 5필드 30개·청소년 답변 6개·검토자 자격 포함 승인 10필드·서명 증거를 받고 canonical checker와 패키지 해시가 모두 맞을 때만 완료로 전파한다. 현재 gate는 pending-valid지만 외부 입력 46칸은 공란이고 review_complete=false다. REQ-001은 완료됐고 REQ-008 재부팅 실증과 C-001 외부 승인만 남아 9 완료/2 검토/총 11을 유지한다. |
| SEO/GEO share cards | pytest app/test_session_share.py app/test_session_turn_persistence.py -q / npm run generate:api-types / npm run typecheck | 21 passed; session share creates hashed-token public unfurl payload without raw transcript, revoked token returns 404, OpenAPI generated share DTOs, review screen share button typechecks. Static robots.txt/sitemap.xml/llms.txt added. |
- | Backend pytest baseline | pytest -q app | 2026-08-28 전체 실행 1002 passed. |
- | X2 evaluator routing/cache/cost trend | 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 / API type generation/check / web typecheck/build / admin focused E2E·layout visual gate / authenticated public API smoke | 최신 backend 439 passed, gateway 45 passed, desktop/mobile E2E 2 passed, 7폭 focused visual gate 1 passed. Claude CLI SDK 비용 추정값과 Agy/Gemini·Codex·Claude API 공식 참조단가를 분리하고, 기존 0달러 행의 조회 시 보정·단가 미등록 모델의 미산정 표시·예산 합산·리포트 경고까지 고정했다. Claude 토큰은 전체 agent tree와 캐시 입력을 포함한다. 과거 0/0 Claude 442건은 로컬 JSONL의 실제 usage와 유일 일치한 169건만 백필했고, 273건은 token_unmetered_turns로 남겼다. 인증된 공개 관리자 화면은 확인 시점 30일 Claude 183건 중 계량 125·미계량 58건을 표시했다. |
+ | Backend pytest baseline | pytest -q app | 2026-08-29 전체 실행 1074 passed / 1 skipped. |
+ | X2 evaluator routing/cache/cost trend | python -X utf8 -m pytest -p no:cacheprovider app/test_llm_pricing.py app/test_admin_ops.py app/test_admin_usage_postgres.py app/test_usage_report.py engine_gateway/test_provider_registry.py engine_gateway/test_gateway_model.py -q / 격리 PostgreSQL / API type generation/check / web typecheck/build / admin focused E2E | 2026-08-28 focused 105 passed/1 skipped, app+gateway 전체 1102 passed/1 skipped, 새 빈 PostgreSQL 온라인 마이그레이션+통합 1 passed, release agent 31 passed, desktop/mobile E2E 6 passed. Claude CLI SDK 비용 추정값과 호출 시점 Agy/Gemini·Codex·Claude API 공식 참조단가를 분리한다. Gemini 3.6/3.7 유효기간, 요청별 200K 구간, 과거 캐시 미보존 행의 reference_upper_bound, 확정 산정/미산정 partial, 상한/미산정 partial_upper_bound, 전체·일별·예산 certainty, 정확한 (e2e, fake-client, input=1, output=1, cost=0) 원장 제외와 리포트 경고를 고정했다. 동일 계량 시그니처 SQL 집계와 migration 18의 concurrent 기간 인덱스로 90일 원장의 raw-row fetch/sort를 제거하고 배포 중 turn 쓰기 잠금을 피한다. 과거 0/0 Claude 442건은 로컬 JSONL의 실제 usage와 유일 일치한 169건만 백필했고 273건은 token_unmetered_turns로 남겼다. 이번 변경은 로컬 검증만 했고 공개 런타임은 미배포다. |
| H3 raw source isolation | python -B -m pytest -p no:cacheprovider app/test_persona_review.py app/test_live_coach_sources.py -q | 38 passed; persona source registration records raw hash-only artifacts separately from sanitized evaluator-only RAG chunks, and rag.index_document() rejects sensitivity=3/raw-marker chunks before DB access. |
| H3 persona itemized authoring UI | npm run typecheck / npm run build / npm run check:api-types / npx playwright test e2e/teacher.spec.ts --project=chromium-single-run --workers=1 / npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run --workers=1 / npx playwright test e2e/session-layout.spec.ts --project=chromium-desktop --project=chromium-mobile --workers=1 | Typecheck/build/API type drift check passed; teacher PersonaStudio focused 8 passed including structured list payload and labeled prompt preview without raw JSON; visual gate 9 passed with the prompt tab opened; session-layout desktop/mobile 8 passed. |
| H2 session review 3-column workbench | npm run typecheck / npm run build / npx playwright test e2e/session-review.spec.ts --project=chromium-desktop --workers=1 / npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run --workers=1 | Typecheck/build passed; 3 focused review E2E passed; 9 visual gates passed with dark-theme assertion. Filled learner review uses overview/flow, transcript, and evaluation rail as a 3-column desktop workbench with worksheet below; empty review remains 2 columns or less to avoid sparse gaps. |
@@ -1152,7 +1162,7 @@
| OAuth/SAML admin landing | C:\Users\encep\AppData\Local\Programs\Python\Python311\python.exe -X utf8 -m pytest -p no:cacheprovider app/test_auth_providers.py -q / public runtime restart | 36 passed; OAuth/SAML callback now normalizes generic saved next paths such as /learn and /teach to /admin for admin-entitled users while preserving deep links such as /learn/session/.... Live proof after commits 998acb44/6b6241f4 and latest boot-diagnostic deploy b67fd5d4: local/public /health returned prod, db=true, engine=true; custom domain now serves assets/index-R5KK7hZI.js/assets/index-DzdAkRsn.css plus boot diagnostic HTML; unauthenticated /personas remains 401. |
| Session read-model DB readiness | python scripts\check-deploy-preflight.py --skip-db --env-file infra\.env.example --allow-placeholder-secrets / https://api-vignette.chanpaca.net/health | 2026-06-29 prod 503 원인은 운영 DB의 app.turns.provider_events 컬럼 누락이었다. 운영 DB hotfix 후 /teacher/dashboard code path는 source=database로 복구됐다. 현재 db.healthcheck(), runtime table readiness, deploy preflight DB mode는 app.turns 음성 메타 컬럼 5개(audio_ref, silence_ms, speech_rate, barge_in, provider_events), app.session_review_status worksheet 컬럼, app.safety_events 필수 컬럼을 함께 검증한다. 2026-06-30에는 Docker Desktop/DB 중단으로 public API 530/error code 1033이 재발했지만, Docker Desktop/DB 재기동 뒤 public health가 environment=prod, db=true, engine=true로 복구됐다. |
| Admin usage persistence | python -m pytest app/test_admin_ops.py app/test_runtime_policy.py -q / authenticated local public-API smoke | 26 passed; /admin/usage returns 200 with source=database, durable=true. Cloudflare blocked raw Python public smoke with 1010, so app-level HTTP was verified against 127.0.0.1:8001 using the same prod process. |
- | Admin AI operations subpage | npx playwright test e2e/admin.spec.ts --project=chromium-desktop --project=chromium-mobile --workers=1 --grep="shows detailed AI metering" / npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run --workers=1 / public-admin-visual.spec.ts --grep="shows the live Codex and Agy model catalogs" | /admin/ai는 7·30·90일 DB 원장, 비용·토큰·토큰 계량 커버리지·호출당 단가, 일별 비용, provider/model breakdown, evaluator cache를 표시하고 엔진 설정을 실제 저장한다. 비용은 SDK 추정·Provider 보고·참조단가·미산정을 구분한다. 토큰 입력은 비캐시·캐시 읽기·캐시 생성 합계이며, 실제 usage가 복구된 행은 토큰과 계량/전체 회 커버리지를, 복구 불가 0/0 행은 미계량을 표시한다. 최신 focused desktop/mobile 2 passed, 7폭 layout 1 passed, 실제 공개 관리자 브라우저 1 passed다. |
+ | Admin AI operations subpage | npx playwright test e2e/admin.spec.ts e2e/full-sweep-admin-ai.spec.ts --project=chromium-desktop --project=chromium-mobile --workers=1 --grep="shows detailed AI metering|shows partial AI cost|does not present a partial upper bound" / app/test_admin_usage_postgres.py | /admin/ai는 7·30·90일 DB 원장, 비용·토큰·토큰 계량 커버리지·호출당 단가, 일별 비용, provider/model breakdown, evaluator cache를 표시하고 엔진 설정을 실제 저장한다. 비용은 SDK 추정·Provider 보고·참조단가·참조 상한·일부 산정·일부 상한·미산정을 구분한다. 과거 캐시 미보존 보정값은 ≤$…, 확정 산정/미산정 합계는 $…+, 상한/미산정 혼합은 미산정으로 표시한다. 전체·일별·예산의 불확실한 평균·비중은 숨기고 예산은 indeterminate를 표시한다. 2026-08-28 새 빈 PostgreSQL 마이그레이션+통합 1 passed와 desktop/mobile 6 passed를 확인했고 공개 관리자 브라우저는 이번 변경에서 재검증하지 않았다. |
| Synthetic health sampler | record_admin_health_sample(), record-admin-health-sample.py, install-health-sampler-task.ps1 | Backend focused 31 passed. Python compile/help passed; PowerShell parser + -PrintOnly passed. Local one-shot appended 5 service rows to app.admin_health_event: status ok, engine_mode claude_cli. This remains sample history, not an SLA claim. |
| Health retention/rollup | admin_health_daily_rollup, maintain-admin-health-events.py, /admin/uptime | Backend focused 36 passed. Python compile/help passed. Local dry-run returned rollup_event_count=0, prunable_event_count=0. Uptime summary now combines raw samples with daily rollups when raw rows have been pruned; detailed events stay raw-only. |
| Ticket duplicate linking | support_ticket.fingerprint, parent_ticket_id, /admin/tickets | Backend focused 38 passed. API types, web typecheck, and build passed. Admin tickets E2E 2 passed after restarting local API 8000 on the new code. Duplicate handling is deterministic hint + admin manual parent link/unlink only; no automatic merge, escalation, issue, PR, or thread creation. |
@@ -1206,10 +1216,10 @@
| Public OAuth start | /auth/config + /auth/login?provider=google | 2026-08-28 public auth config 200, Google configured true, allowed_email_domains=[], redirect URI https://api-vignette.chanpaca.net/auth/callback, dev-login disabled. 로그인 DOM은 모든 Google 계정을 명시하고 실제 Google 선택기에 yunchan@twentyoz.kr와 yunchan8804@gmail.com이 함께 노출됐다. 기존 Google 계정 선택→callback→/admin 성공을 확인했다. 신규 Gmail 선택은 Vignette 계정 생성이므로 사용자 행동시점 확인 뒤 별도 실증한다. |
| Persona auth boundary | GET /personas | 2026-06-30 복구 후 public unauth /personas는 401 not authenticated를 반환한다. |
| Public login | auth.spec.ts --grep public login | 1 passed |
- | Public runtime scripts · source pin | start/watch/boot-public-runtime*.ps1 · install-public-runtime-task.ps1 · register-boot-task.ps1 | 2026-08-28 detached-clean 99779a6a…fbc8·tree 57da86aa…6b91에 로그온·watchdog 두 task를 재등록했다. action working directory/root/commit/tree/SHA pin, PT1H, IgnoreNew, scheduler retry 0은 PASS다. focused 회귀 50/50 뒤 watchdog 18:10:57 KST·boot 18:14:27 KST 수동 실행은 모두 LastTaskResult=0, failcount 0이었다. 실제 Windows 재부팅 자동복구 smoke는 별도 운영 gate다. |
- | Public API health | http://127.0.0.1:8001/health / https://api-vignette.chanpaca.net/health | 2026-08-28 17:11:47 KST local/public 모두 status=ok·environment=prod·db=true·engine=true. voice는 local/public 모두 local_whisper small·melotts-korean·WS queue 4, web은 local/public 200과 동일 asset index-DJPgoeJy.js/index-CXOy3QyV.css를 반환했다. DB는 백업 vignette-dev-db-vignette-20260828-065331Z.dump를 검증하고 recovered named volume을 보존한 채 migration 17을 적용했다. |
- | Public runtime current snapshot | health + provenance + Scheduled Tasks + Pages | 현재 동작 중인 API/engine/web/STT/TTS/cloudflared와 Cloudflare Pages production 1f1ddf18-9d5c-4d83-95a7-8cd71245e646은 clean source 2a396361…89c 계열이다. 다음 장애/로그온 복구 task만 99779a6a…fbc8에 pin했다. 새 학습자 헤더를 포함한 dist는 714파일·assets 164개로 빌드하고 최근 production 5세대를 보존했지만, Pages upload는 명시 승인이 없어 실행하지 않았다. |
- | Public/local/Tailnet login recovery | https://vignette.chanpaca.net/login / https://api-vignette.chanpaca.net/health / https://alpaca-home.taile93291.ts.net/login | 2026-08-28 public web 200, public API health 200, auth config 200, Google login start와 callback 성공, P20 실제 회기·응답·종료·평가 폐루프 성공. Earlier Tailnet checks remain recorded separately; vnet DNS A records are still 0. |
+ | Public runtime scripts · source pin | start/watch/boot-public-runtime*.ps1 · install-public-runtime-task.ps1 · register-boot-task.ps1 | 2026-08-29 최신 read-only 확인에서 두 task는 old detached dba9b75a… runtime을 계속 가리킨다. Docker daemon 부재로 runtime task는 실패했고 watchdog도 DB/API를 복구하지 못한다. 새 total-size/decode receipt 후보는 미배포이며, 후보 통합 GREEN·사용자 승인 전 task 재등록이나 runtime 재시작을 하지 않는다. 실제 Windows 재부팅 자동복구 smoke도 별도 운영 gate다. |
+ | Public API health | http://127.0.0.1:8001/health / https://api-vignette.chanpaca.net/health | 2026-08-29 최신 상태는 OUTAGE다. local 8001은 연결 거부, public API는 530이며 Docker Desktop Linux engine pipe가 없다. 정적 public Web 200은 API·DB GREEN을 뜻하지 않는다. 2026-08-28의 status=ok·db=true·engine=true와 voice exact 값은 과거 성공 증거로만 보존한다. |
+ | Public runtime current snapshot | health + provenance + Scheduled Tasks + Pages | 배포된 동적 런타임/task 기준선은 clean commit dba9b75a…·tree 14cd4607…, Cloudflare Pages production은 0c60261e-cb37-482d-ba42-d91586194c48(source 5bf89ff4…)다. 현재 API/DB는 장애이고 fail-closed avatar 후보와 새 upload receipt는 아직 이 기준선에 승격되지 않았다. 후보와 deployed baseline을 분리하며, 승인 전 Docker/API/tunnel/task/DB/upload-root mutation을 하지 않는다. |
+ | Public/local/Tailnet login recovery | https://vignette.chanpaca.net/login / https://api-vignette.chanpaca.net/health / https://alpaca-home.taile93291.ts.net/login | 2026-08-28 Google callback→관리자·P20 실제 회기 폐루프는 유효한 과거 증거다. 2026-08-29에는 public API 530으로 신규 callback·관리자·G8 실DB E2E를 재검증할 수 없으므로 현재 운영 GREEN으로 재사용하지 않는다. public Web은 정적 로그인 화면만 200이다. |
| Local 5175 login | PLAYWRIGHT_BASE_URL=http://127.0.0.1:5175 auth.spec.ts | desktop/mobile passed |
| Learner/readiness E2E | learner.spec.ts + readiness.spec.ts desktop/mobile | 14 passed |
| Learner growth header | uc-learner-home-dashboard.spec.ts --grep "성장 지표 라포 헤더" | 2026-08-28 chromium desktop/mobile 2 passed. 라포 문구 부모를 inset surface에서 plain div로 바꾸고 computed background transparent·border/radius/padding 0을 고정했다. typecheck·design SSOT·cosmetic filter safety도 통과했으며 Pages에는 아직 미배포다. |
@@ -1249,7 +1259,7 @@ $env:E2E_PUBLIC_STORAGE_STATE=".\node_modules\.tmp\public-auth.json"
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\start-public-runtime.ps1
닫은 항목(live 증거): ① engine config 운영값(claude_cli / 9099 / gateway-default, durable, db). ② 상주 엔진풀 probe — 단일 session_id 2회 reused stream, TTFT 1667–4199ms, cost 누적 0.068→0.123. ③ 상주 엔진풀 RSS — gateway session 1개에서 claude -p child RSS 363.3MB, gateway RSS 16.2MB. ④ Postgres RLS/audit smoke 5 checks PASS. ⑤ turn cost telemetry — app.turns client_ai 13행에 provider/model/cost_usd 실적재(합 $1.22). ⑥ 레이아웃 시각 게이트 최신 9/9 + 2026-06-28 적대적 재검수 7/7 accept.
- 환경·외부 승인 때문에 아직 못 닫는 항목(정직 표기): 새 학습자 헤더 Cloudflare Pages production upload 명시 승인 · 신규 Gmail 선택→Vignette 계정 생성은 사용자 행동시점 확인 필요 · C-001 사례별 판정·근거·수정 요구·재검토·검토일 30개, 청소년 답변 6개, 임상 검토자 이름·소속·자격·결정 근거를 포함한 승인 10필드와 서명 증거/해시·검토본 고정 · vnet.18ka.net/api-vnet.18ka.net DNS·OAuth redirect 등록 · 한신대 데이터/SSO 거버넌스 · G7 명시 동의 물리 마이크 3,120초와 공통 3,000초 운영 high-water, 독립 human voice-gain pack, canonical checker exit 0 · claude_cli↔Messages API 폴백 동일성 · 실제 Windows 재부팅 후 watchdog smoke · Phase 3 파일럿 게이트. C-001 내부 검토 패킷·canonical 판정기·상한 수리와 pending-valid 검증, current source 공개 배포, generic Google callback, P20 실제 턴·종료·평가, local/public voice exact ready, authenticated WSS 무마이크 rehearsal은 완료했다. 운영 원칙상 가짜 증거로 DONE 표기하지 않는다.
+ 환경·외부 승인 때문에 아직 못 닫는 항목(정직 표기): public API/DB 장애 복구와 avatar fail-closed 후보의 전체 회귀·배포·실브라우저 fallback · G8 관리자 사람 게이트의 승인형 실DB append-only 증거와 public proof(local stateful in-app browser는 GREEN) · C-001 사례별 판정·근거·수정 요구·재검토·검토일 30개, 청소년 답변 6개, 임상 검토자 이름·소속·자격·결정 근거를 포함한 승인 10필드와 서명 증거/해시·검토본 고정 · vnet.18ka.net/api-vnet.18ka.net DNS·OAuth redirect 등록 · 한신대 데이터/SSO 거버넌스 · G7 명시 동의 물리 마이크 3,120초와 공통 3,000초 운영 high-water, 독립 human voice-gain pack, canonical checker exit 0 · claude_cli↔Messages API 폴백 동일성 · 실제 Windows 재부팅 후 watchdog smoke · Phase 3 파일럿 게이트. REQ-001은 Gmail 인증 세션·관리자·복구 데이터 수락으로 완료했다. C-001은 pending-valid 기술 사전검증뿐이며 외부 검수 입력을 조작하지 않는다. 운영 원칙상 가짜 증거로 DONE 표기하지 않는다.
윤찬 결정 상세 (전건 확정)
람다가 단독으로 정하면 월권인 항목이었다. 2026-06-30 owner 결정으로 전건 확정됐고, 각 결정의 구현·실측은 후속 build/GATE로 남는다.
결정 0개(전건 확정)조건부 GO 사유
diff --git a/docs/guides/architecture.md b/docs/guides/architecture.md
index fd99a40..7f8d304 100644
--- a/docs/guides/architecture.md
+++ b/docs/guides/architecture.md
@@ -237,11 +237,25 @@ RBAC×AIView로 차단된다. 이 모듈은 평가 신호만 산출한다.
cache key·prompt·completion 없이 enabled/entries/hits/misses/stores/evictions/requests/hit_rate와
일별 `daily_cost` bucket(day, turns, tokens, cost)을 관리자 관측값으로 반환한다. 비용은
공급자가 반환한 저장값을 우선하되 Claude CLI는 SDK 추정치(`provider_estimate`)로 명시하고,
- 저장 비용이 없는 Agy/Gemini·Codex·Claude API는 `app.services.llm_pricing`의 버전 고정 공식 참조단가(`reference_rate`)로
- 입력·캐시 입력·출력 토큰을 환산한다. 기존 DB에 비용 0으로 저장된 행도 조회 시 같은 단가로
- 보정하며, 단가가 없는 모델은 0달러로 위장하지 않고 `unavailable`로 표시한다.
- 응답은 `recorded_cost_usd`, `estimated_cost_usd`, provider/model별 `cost_basis`,
- `rate_label`, `rate_source_url`을 함께 반환하고 예산 상태는 두 비용을 합친 유효 비용을 사용한다.
+ 신규 Agy/Gemini·Codex·Claude API 호출은 `app.services.llm_pricing`의 호출 시작일 기준 공식
+ 참조단가(`reference_rate`)로 입력·캐시 입력·출력 토큰을 환산한다. Gemini 3.6/3.7 Flash는
+ 유효기간이 있는 단가 구간을 사용하고, 기존 DB의 비용 0 행은 일별 합산 전에 요청별로 다시
+ 가격화해 200K 같은 요청 단위 경계를 보존한다. 과거 행은 캐시 입력 수가 따로 남아 있지 않아
+ 전체 입력 단가로 계산한 보수적 상한(`reference_upper_bound`, UI `참조 상한`)으로 표시한다.
+ 호출별 확정 산정액과 미산정액이 섞이면 `partial`과 `$…+`로 표시한다. 상한 추정과 미산정이
+ 함께 있으면 방향을 단정할 수 없으므로 `partial_upper_bound`와 `미산정`으로 표시하며, 단가가
+ 전혀 없는 모델도 0달러로 위장하지 않고 `unavailable`로 표시한다. 양수로 저장된 과거 참조
+ 비용은 현재 단가 라벨을 다시 붙이지 않고 `호출 시점에 저장된 참조단가 추정값`으로만 설명한다.
+ 응답은 전체·일별·provider/model·예산에 `cost_basis`를 전파하고 `recorded_cost_usd`,
+ `estimated_cost_usd`, `rate_label`, `rate_source_url`을 함께 반환한다. 부분 산정·상한 상태에서는
+ 호출당 비용·1천 토큰당 비용·비용 비중을 확정값처럼 계산하지 않으며 예산도 `indeterminate`를
+ 사용할 수 있다. DB 조회는 동일한 day/provider/model/token/cost 시그니처만 묶어 요청별 단가
+ 경계를 보존하고, migration 18의 client-turn `created_at` partial index로 기간 조회를 제한한다.
+ 이 인덱스는 release agent가 `CREATE INDEX CONCURRENTLY`로 트랜잭션 밖에서 적용해 턴 쓰기를
+ 막지 않으며, 다른 schema migration은 기존 단일 트랜잭션 적용을 유지한다.
+ 제어된 음성 E2E가 남긴 정확한 `(e2e, fake-client, input=1, output=1, cost=0)` 조합은 비용 원장·예산에서만 제외하며,
+ 다른 `e2e` provider/model 조합이나 원본 DB 행을 포괄 삭제하지 않는다. 음성 성공 E2E의
+ 전용 disposable PostgreSQL 격리는 후속 항목이며, 공유 DB를 직접 삭제하는 cleanup은 사용하지 않는다.
`EVALUATOR_SEMANTIC_CACHE_ENABLED`, `EVALUATOR_SEMANTIC_CACHE_TTL_SECONDS`,
`EVALUATOR_SEMANTIC_CACHE_MAX_ENTRIES`로 제한하며, 원문 prompt/completion은 캐시에 저장하지 않는다.
cache hit은 `engine.generate()`와 metadata-only `audit.llm_call_log` 기록을 건너뛰고,
@@ -1053,10 +1067,29 @@ DB 레벨 이중강제(`04_audit_eval_rls.sql` §5, `app/db.py` `acquire()`):
렌더링하지 않고, 빈 상태 카드 대신 가입 직후 사용자 정보를 입력하는 단순 폼만 보여준다.
이 화면은 이메일을 다시 받지 않고 닉네임, 자기소개,
선택 아바타 이미지, 이름, 소속, 학과, 학년/직위, 연락처, 주소/수령지와 서비스 이용약관·개인정보
- 처리방침 초안 동의를 저장한다. 아바타 파일은 `POST /users/me/avatar`가 MIME/시그니처/3MB 제한을
- 확인한 뒤 `USER_UPLOAD_DIR/profile-avatars`에 저장하고 URL만 `app_user.avatar_url`에 보관한다.
+ 처리방침 초안 동의를 저장한다. 아바타 파일은 `POST /users/me/avatar`가 MIME/시그니처/3MB 제한에 더해
+ Pillow full decode와 확장자-format 일치를 확인한 뒤 `USER_UPLOAD_DIR/profile-avatars`에 저장하고 URL만
+ `app_user.avatar_url`에 보관한다. 시그니처만 닮고 실제 decode가 실패하는 payload는 저장 전에 거부한다.
학습자 `POST /sessions`와 dev 음성 persona 시작은 온보딩 완료 후에만 허용하며, 온보딩 저장 시
learner `consent_at`도 함께 세팅한다.
+- public avatar storage는 `USER_UPLOAD_DIR/profile-avatars`만 `/uploads/profile-avatars`에 mount한다. 같은
+ `USER_UPLOAD_DIR`의 `multimodal-audio`는 private adapter 전용이며 static mount와 public migration에서 제외한다.
+ public runtime은 source/upload root 밖 private migration receipt(SHA/root binding)와 현재 DB의 모든 managed avatar
+ 참조 파일(regular/no-reparse/readable)을 기동마다 검증한다. 초기 8개 migration snapshot과 현재 set은 동일할 필요가
+ 없지만, non-empty initialization 뒤 current set이 0이 되려면 별도 reset receipt가 필요하다.
+- upload manifest v3는 object count뿐 아니라 전체 byte 합계·inventory SHA-256·객체별 `decode_valid`와 preserved/current
+ decode 정상/실패 count를 결속한다. immutable public cache에는 decode-valid bytes만 올리고, preserved이지만 decode-invalid인
+ legacy payload는 forensic 보존하되 static handler가 404로 차단해 프론트 fallback을 사용하게 한다. 경로 존재·PNG signature만으로
+ 정상 이미지를 선언하지 않는다. manifest 이후 생성된 UUID형 업로드도 DB URL 기록·현재 참조 검증·GET/HEAD마다 용량 제한과
+ Pillow full decode·확장자-format 일치를 다시 확인하고, 검증한 동일 bytes를 응답해 검사 뒤 재오픈 경쟁을 막는다. 이후 손상된
+ 신규 파일은 current invalid count와 health fallback에 반영되고 public 404가 된다. 2026-08-29 감사 기준은 union 93 objects·
+ 52,973 bytes·`9d703126…e6aa`, decode 정상 3/
+ 실패 90, 현 DB 참조 정상 2/실패 6/missing 0이다. 이 계약은 후보 코드이며 public API 장애 복구·전체 E2E·정식 승격 전에는
+ deployed runtime의 동작으로 간주하지 않는다.
+- 프로필 업로드는 새 random target을 create-new 방식으로 완전히 flush한 뒤 publish하고 기존 DB 참조 파일을 선삭제하지
+ 않는다. upload 뒤 PATCH가 실패해도 기존 URL의 파일은 남으며, unreferenced object 정리는 별도 manifest-aware GC가
+ 소유한다. fresh storage cutover는 API write-lease freeze가 active/valid/drained임을 증명하고 tunnel-first로 외부 write를
+ 닫은 뒤 진행한다.
- 관리자/교수자 권한은 온보딩 화면에서 신청받지 않는다. 서버는 `AUTH_ADMIN_EMAILS` /
`AUTH_TEACHER_EMAILS` allowlist와 관리자 사용자 관리 경로로만 역할을 부여한다.
- 로컬/Tailnet dev는 dev-login을 사용한다. 공개 `OAUTH_REDIRECT_URI`가 로컬/Tailnet 세션이 아니라 public API
diff --git a/docs/guides/local-development.md b/docs/guides/local-development.md
index 56b997a..a48a399 100644
--- a/docs/guides/local-development.md
+++ b/docs/guides/local-development.md
@@ -91,13 +91,29 @@ prod API 8001, web preview 5174, engine gateway 9099, cloudflared tunnel을 검
worktree를 만들고, 그 root 내부 registrar에 `-StableSourceRoot`를 명시해 두 task를 승격한다.
$releaseRoot = 'D:\workspace\vignette-public-runtime-
'
+ $userUploadDir = Join-Path $env:LOCALAPPDATA 'Vignette\public-runtime\uploads'
+ $uploadStateDir = Join-Path $env:LOCALAPPDATA 'Vignette\public-runtime\private-state'
+ $uploadManifestSha = ''
+ $uploadManifestPath = Join-Path $uploadStateDir "public-avatar-upload-$uploadManifestSha.json"
+ $uploadFreezePath = Join-Path $uploadStateDir 'avatar-cutover.freeze.json'
$bootRegistrar = Join-Path $releaseRoot 'scripts\register-boot-task.ps1'
- & powershell.exe -NoProfile -ExecutionPolicy Bypass -File $bootRegistrar -StableSourceRoot $releaseRoot
+ & powershell.exe -NoProfile -ExecutionPolicy Bypass -File $bootRegistrar `
+ -StableSourceRoot $releaseRoot -UserUploadDir $userUploadDir `
+ -UserUploadManifestPath $uploadManifestPath `
+ -ExpectedUserUploadManifestSha256 $uploadManifestSha `
+ -UserUploadWriteFreezePath $uploadFreezePath
$watchdogInstaller = Join-Path $releaseRoot 'scripts\install-public-runtime-task.ps1'
- & powershell.exe -NoProfile -ExecutionPolicy Bypass -File $watchdogInstaller -StableSourceRoot $releaseRoot -IntervalMinutes 5
+ & powershell.exe -NoProfile -ExecutionPolicy Bypass -File $watchdogInstaller `
+ -StableSourceRoot $releaseRoot -UserUploadDir $userUploadDir `
+ -UserUploadManifestPath $uploadManifestPath `
+ -ExpectedUserUploadManifestSha256 $uploadManifestSha `
+ -UserUploadWriteFreezePath $uploadFreezePath -IntervalMinutes 5
- `VignettePublicRuntime`은 로그온 Docker/DB 복구, `VignettePublicRuntimeWatchdog`은 로그온+5분 반복 health/recovery다.
-- 두 task action은 release root와 Git commit/tree, boot 또는 watchdog SHA-256, start script SHA-256을 고정한다.
+- 두 task action은 release root와 Git commit/tree, boot 또는 watchdog SHA-256, start script SHA-256,
+ source 밖의 절대 `USER_UPLOAD_DIR`, private migration manifest SHA, source/upload root 밖의 freeze path를 고정한다.
+ consumer는 빈 upload root를 만들지 않으며 watchdog `-CheckOnly`는 정확한 listener PID의 cwd/env와 현재 DB가 참조하는
+ 모든 profile avatar의 존재·regular/no-reparse/readability를 다시 증명한다.
등록 뒤 action marker와 working directory를 읽어 검증하기 전에는 `RunNow`를 호출하지 않는다.
- secret은 task 인자에 넣지 않는다. API secret은 stable release의 `apps/api/.env`, cloudflared/Claude CLI credential은
사용자 profile에 둔다. `.env` 내용을 console이나 evidence에 출력하지 않는다.
@@ -368,8 +384,13 @@ C:\Users\encep\AppData\Local\Programs\Python\Python311\python.exe scripts\mainta
- 신규 사용자 또는 온보딩 미완료 사용자는 로그인 직후 `/onboarding`에서 닉네임, 자기소개,
선택 아바타 이미지, 이름, 소속, 학과, 학년/직위, 전화번호, 주소/수령지와 약관·개인정보
동의를 저장해야 역할 홈으로 이동한다. 학습자 회기 시작은 온보딩 완료와 동의가 모두 있어야 한다.
-- 프로필 아바타 업로드는 API 작업 디렉터리 기준 `USER_UPLOAD_DIR`(기본 `uploads`) 아래
- `profile-avatars/`에 저장되고, `/uploads/profile-avatars/...` URL로 서빙된다.
+- 프로필 아바타 업로드는 `USER_UPLOAD_DIR` 아래 `profile-avatars/`에 저장되고,
+ `/uploads/profile-avatars/...` URL로 서빙된다. 로컬 dev에서 변수를 생략하면 API 작업 디렉터리 기준
+ `uploads`를 쓰지만, Windows public runtime은 release root 밖의 절대
+ `%LOCALAPPDATA%\Vignette\public-runtime\uploads`를 task와 API process environment에 명시한다. API는 전체 root가
+ 아니라 `profile-avatars`만 static mount하므로 private `multimodal-audio`가 `/uploads`로 노출되지 않는다. public
+ runtime의 manifest/freeze는 upload root 밖 private state directory에 두며, 정확한 초기화·cutover 순서는
+ `docs/ops/public-runtime-watchdog.md`를 따른다.
> 주의(이메일 도메인): 이 제한은 dev-login·SAML 조직 정책에만 적용된다. dev-login은
> 이메일 도메인이 `AUTH_ALLOWED_EMAIL_DOMAINS`에 있어야 한다. 예: `learner@hs.ac.kr`.
diff --git a/docs/guides/source-docs-and-gaps.md b/docs/guides/source-docs-and-gaps.md
index b3a3647..45a619f 100644
--- a/docs/guides/source-docs-and-gaps.md
+++ b/docs/guides/source-docs-and-gaps.md
@@ -72,7 +72,7 @@
| **M2** | 다회기 종단 케이스 아크·교차회기 사례개념화 11차 구동 | `(persona_id, learner_id)` 안정 `case_profile` upsert, 원자적 `session_no`, voice/REST/SSE recall cache 주입을 연결했다. 세션 종료 시 마스킹 축어록 기반 fallback `session_summary.digest`, `case_profile.case_digest`, `rapport_trajectory`, `alliance_level`을 갱신하고, 다음 회기 seed recall은 `case_digest`·직전 `session_summary`·client-visible non-contradicted `pinned_fact`를 함께 조립한다. 3차에서는 마스킹된 client-visible 발화에서 `[NAME]`/`[ORG]` identity와 명시적 상담 약속만 보수적으로 `pinned_fact`에 upsert한다. 4차에서는 pinned fact 삽입 또는 값 변경 시 `pinned_fact_history`에 append-only 이력을 남긴다. 5차에서는 명시적 상담 약속 철회/부정만 기존 non-locked `agreement:counseling` fact를 `contradicted`로 격리하고 history reason `contradiction`을 남긴다. 6차에서는 세션 종료 저장 성공 뒤 마스킹된 client-visible 내담자 발화만 `app.turn_embedding`에 BGE-M3 dense/sparse로 idempotent 색인한다. 7차에서는 REST submit/SSE stream의 턴 컨텍스트 조립 경계를 `_prepare_turn_context()`로 묶고, DB seed recall과 pinned fact가 다음 턴 EngineMessage L2/L4에 raw 이름 마스킹 상태로 주입되는 route-level 회귀를 추가했다. 8차에서는 `SessionDigestInput`/`SessionDigestResult`/`SessionSummaryWrite`로 종료 digest 입력·fallback 결과·DB write 인자 경계를 명시해 LLM worker 후보가 raw text, evaluator-only turn, 평가 payload, CCD, deterministic carry를 압축 prompt에 우회 주입하지 못하게 했다. 9차에서는 `DigestQualityAssessment`/`SessionDigestWorkerOutcome`로 local quality harness를 추가해 빈/짧은 digest, raw forbidden substring, 내부 평가·CCD·상태 marker, 잘못된 `S{session_no}:` prefix를 fallback 유지 대상으로 판정한다. 10차에서는 `session_digest_worker.py`가 `CompressionJob`을 Node-compatible `GenerateRequest`/`EngineMessage`로 변환하고, 주입형 engine/audit 호출 뒤 quality gate 통과 결과만 `session_summary.digest/compressed_by/token_count`와 `case_profile.case_digest`에 적용하는 one-shot 경계를 소유한다. 11차에서는 `scripts/run-session-digest-worker.py` dry-run/apply runner와 `SESSION_DIGEST_WORKER_ENABLED=false` 기본값의 세션 종료 background scheduler 골격을 붙였다. scheduler는 DB load/apply 때만 connection을 잡고 engine 호출은 transaction 밖에서 수행하며, `compressed_by IS NULL` loader/apply CAS로 재실행 race를 막는다. DB loader는 persisted fallback summary와 client-visible `text_masked` transcript만 재구성하며 raw `text`, evaluator-only turn, CCD, end_state는 prompt에 넣지 않는다. `digest_pending`은 CompressionJob 생성 여부를 알리는 비동기 압축 필요 신호로 유지한다. 같은 값 재확인은 history를 늘리지 않고, `locked` fact는 건드리지 않는다. 관계갈등·위기·임상 추론은 자동 pinning/모순 처리에서 제외한다. | 관계·임상 fact 승격 기준, 실 provider 장시간 운영, 임상 골든셋 품질평가, 재압축, 접수면접→다회기 연속성·자기개념 진화 실증. |
| **M3** | SSO claim 매핑·식별자 안정성 1차 완료·운영 IdP 감사 미연결 | Google/SAML/dev-login이 `AUTH_EMAIL_COHORT_MAP`·`AUTH_DOMAIN_COHORT_MAP` 및 SAML cohort claim을 `cohort_ids`로 전달하고, `app_user.external_id`는 provider subject(`google:`/`saml:`/`dev:`) 기반으로 저장한다. 운영 SAML 서명검증, 기관 claim schema/test tenant, deprovisioning audit은 아직 없다. | 한신 IdP 확정 후 SAML 서명검증, claim→role/cohort/institution_user_id 매핑 표 실연동, role변경/삭제 audit, deprovisioning evidence. |
| **X1** | 재귀학습·데이터셋 export 파이프라인 2차 구현 | `scripts/export-recursive-dataset.py`와 `app.services.dataset_export`로 masked-text JSONL dry-run, PII scan, kappa/ICC 계산, approved export 게이트를 구현했다. exporter는 consent가 남아 있고 client-visible인 `text_masked` 턴만 고르며, supervisor comment raw text를 JSONL에 넣지 않는다. `scripts/check-phase3-artifacts.py`는 approved와 dry-run dataset JSONL의 required keys, row count, privacy, PII shape를 검사해 `{}` 한 줄 같은 false-positive를 막는다. 기본은 `technical_dry_run`이며 실제 승인 export·골든셋 승격은 데이터 steward/legal review와 IAA 통과가 필요하다. | 파일럿 evidence에서 reviewer disposition, steward/legal 승인, gold annotation 라운드 적재, withdrawal/consent roster 대조, `min_completed_sessions` 정책 반영 후 `approved_for_recursive_learning_seed` 승격 검증. |
-| **X2** | AI API 비용 관측·예산 경고·평가 저비용 라우팅·evaluator cache 관측·일별 비용 추이·모델별 비용 검증 리포트 3차 완료 | 턴별 provider/model/tokens/cost 저장 경로와 `GET /admin/usage`, 관리자 비용 대시보드를 연결했다. `ADMIN_USAGE_BUDGET_USD` 기준 예산 상태(ok/warn/exceeded)도 응답/UI에 표시한다. `EVALUATOR_FAST_MODEL`/`EVALUATOR_DEEP_MODEL` 설정 시 fast/deep 평가 호출만 해당 모델 override로 gateway에 전달하고, 비워두면 기존 gateway default 라우팅을 유지한다. fast/deep evaluator structured 결과는 canonical request SHA-256 기반 인메모리 semantic cache로 재사용하며, 원문 prompt·completion은 저장하지 않고 성공 파싱 결과만 TTL/entry 제한 안에서 캐시한다. `/admin/usage`와 관리자 비용 카드가 cache enabled/entries/hits/misses/stores/evictions/requests/hit_rate와 일별 `daily_cost` 추이를 노출한다. `app.services.usage_report`와 `scripts/report-ai-usage.py`는 같은 usage JSON에서 provider/model별 cost share, token share, cost/turn, cost/1k tokens, metered coverage, budget/cache warning을 산출한다. 3차에서는 Claude CLI SDK 비용 추정값을 우선하고, 비용이 없는 Agy/Gemini·Codex·Claude API는 버전 고정 공식 참조단가로 입력·캐시 입력·출력 토큰을 환산한다. Claude 토큰은 `modelUsage` 전체 agent tree와 cache read/create 입력을 합산한다. 과거 미수집 442건은 비용 역산 없이 로컬 Claude JSONL의 정규화 응답 SHA-256·생성 시각이 유일하게 일치한 169건만 실제 usage로 백필했고, 불일치 273건은 `미계량`으로 남겼다. 기존 DB의 0달러 행도 조회 시 보정하고, 단가 미등록 모델은 `미산정`으로 명시한다. DB 미가용 dev는 runtime store fallback, prod는 fail-closed다. | 자동 차단·한도 enforcement 정책, 공식 단가 변경 시 rate card 갱신·회귀. |
+| **X2** | AI API 비용 관측·예산 경고·평가 저비용 라우팅·evaluator cache 관측·일별 비용 추이·모델별 비용 검증 리포트 4차 완료 | 턴별 provider/model/tokens/cost 저장 경로와 `GET /admin/usage`, 관리자 비용 대시보드를 연결했다. `ADMIN_USAGE_BUDGET_USD` 기준 예산 상태(ok/warn/exceeded/indeterminate)도 응답/UI에 표시한다. `EVALUATOR_FAST_MODEL`/`EVALUATOR_DEEP_MODEL` 설정 시 fast/deep 평가 호출만 해당 모델 override로 gateway에 전달하고, 비워두면 기존 gateway default 라우팅을 유지한다. fast/deep evaluator structured 결과는 canonical request SHA-256 기반 인메모리 semantic cache로 재사용하며, 원문 prompt·completion은 저장하지 않고 성공 파싱 결과만 TTL/entry 제한 안에서 캐시한다. `/admin/usage`와 관리자 비용 카드가 cache enabled/entries/hits/misses/stores/evictions/requests/hit_rate와 일별 `daily_cost` 추이를 노출한다. `app.services.usage_report`와 `scripts/report-ai-usage.py`는 같은 usage JSON에서 provider/model별 token share와 계량 coverage를 산출하며, cost share/cost-per-unit은 비용 확실성이 보장될 때만 산출한다. Claude CLI SDK 추정값을 우선하고 Agy/Gemini·Codex·Claude API는 호출 시작일 기준 공식 참조단가를 쓴다. 4차에서는 Gemini 3.6/3.7 Flash의 출시일·2026-08-13 프로모션·2027-01-01 표준 단가를 유효기간으로 분리하고, 과거 0달러 행을 동일 계량 시그니처별로 묶어 요청별 가격화해 입력 200K 단가 경계를 보존한다. 캐시 토큰이 별도 보존되지 않은 과거 보정액은 `reference_upper_bound`, 확정 산정/미산정 혼합은 `partial`, 상한/미산정 혼합은 `partial_upper_bound`, 완전 미산정은 `unavailable`로 구분한다. 전체·일별·예산에도 같은 `cost_basis`를 전파한다. 정확한 `(e2e, fake-client, input=1, output=1, cost=0)` 테스트 조합만 보고서에서 제외한다. migration 18은 client turn 기간 조회용 partial index를 `CREATE INDEX CONCURRENTLY`로 트랜잭션 밖에서 추가한다. Claude 토큰은 `modelUsage` 전체 agent tree와 cache read/create 입력을 합산한다. 과거 미수집 442건은 비용 역산 없이 로컬 Claude JSONL의 정규화 응답 SHA-256·생성 시각이 유일하게 일치한 169건만 실제 usage로 백필했고, 불일치 273건은 `미계량`으로 남겼다. DB 미가용 dev는 runtime store fallback, prod는 fail-closed다. | 자동 차단·한도 enforcement 정책, 공식 단가 변경 자동 감시, `priced_at`/`rate_id`/캐시 입력 토큰의 영속 provenance, 음성 E2E 전용 disposable DB 격리. |
| **L1** | 기술스택 신청서-구현 불일치 및 단기일정 산출물 압박 | doc4 신청서 스택(Spring Boot 3/Node.js·TimescaleDB) vs 실제 FastAPI/Python 불일치, 20주 단기일정·9월 저작권 등재 압박. 소유자 결정으로 장기 교체 대상은 Node.js 우선, 현재 FastAPI 전면 재작성은 보류했다. 내부 전환 증거로 engine gateway 공유 계약, `EngineClient.stream_packets()` decode 경계, schema-backed golden fixture(`engine_gateway_contract.v1.json`/`engine_gateway_schema.v1.json`), Python import 없는 `scripts/check-engine-gateway-contract.mjs` Node.js conformance runner, `gateway-default` default-routing sentinel 정규화, `structured_payload_from_response()` 기반 structured/legacy JSON response parser, 브라우저-facing 세션 read-model 분리(`app/session_read_model.py`), 페르소나 DTO/mapper 분리(`app/persona_read_model.py`), 페르소나 draft generation 계약(`app/persona_generation_contract.py`), session evaluation write packet(`SessionEvaluationWrite`), stage 라벨/phase-key 정규화 SSOT(`app/stage_contract.py`)까지 고정했다. | 신청서/저작권 등재 문서에 FastAPI 유지 사유와 계약 우선 Node 전환 계획을 반영하는 외부 거버넌스 증거. 내부 후보였던 H3 항목형 목록 저작 UI와 프롬프트 미리보기 de-JSON은 10차에서 완료. |
> X2 근거: doc3 회의록이 'AI API 비용'을 운영 리스크로 명시.
diff --git a/docs/guides/testing.md b/docs/guides/testing.md
index 349bb07..ebf26a9 100644
--- a/docs/guides/testing.md
+++ b/docs/guides/testing.md
@@ -15,12 +15,12 @@ Vignette 저장소의 모든 검증 수단(백엔드 단위 테스트, 웹 타
| 검증 | 작업 디렉터리 | 명령 | DB | API(8000) | 웹(5173) | 엔진GW(9099) | 브라우저 | 현재 통과 |
|---|---|---|---|---|---|---|---|---|
-| 백엔드 단위 테스트 | `apps/api` | `python -m pytest app/ -q` | 불필요 | 불필요 | 불필요 | 불필요 | 불필요 | 2026-08-28 전체 실행 1002 passed |
+| 백엔드 단위 테스트 | `apps/api` | `python -m pytest app/ -q` | 불필요 | 불필요 | 불필요 | 불필요 | 불필요 | 2026-08-29 전체 실행 1074 passed / 1 skipped |
| 엔진 게이트웨이 테스트 | `apps/api` | `python -m pytest engine_gateway/ -q` | 불필요 | 불필요 | 불필요 | 불필요 | 불필요 | 2026-08-28 전체 실행 68 passed |
| API 타입 생성 체크 | `apps/web` | `npm run check:api-types` | 불필요 | 불필요 | 불필요 | 불필요 | 불필요 | pass |
| 웹 타입체크 | `apps/web` | `npm run typecheck` | 불필요 | 불필요 | 불필요 | 불필요 | 불필요 | pass |
| 웹 빌드 | `apps/web` | `npm run build` | 불필요 | 불필요 | 불필요 | 불필요 | 불필요 | pass |
-| Playwright E2E(전체) | `apps/web` | `npm run e2e` | **필요(+시드)** | **필요** | 자동기동 | 일부만 | **필요** | 현재 수집 1138 tests / 62 files · 현 작업트리 전체 GREEN 미검증 |
+| Playwright E2E(전체) | `apps/web` | `npm run e2e` | **필요(+시드)** | **필요** | 자동기동 | 일부만 | **필요** | 현재 수집 1158 tests / 64 files · 현 작업트리 전체 GREEN 미검증 |
핵심 원칙: **단위 테스트(pytest)와 타입체크/빌드는 외부 서비스 없이 단독 실행된다.**
**E2E만 풀스택(DB+API+웹+브라우저)을 요구한다.** 아래 각 절에서 근거와 절차를 설명한다.
@@ -42,7 +42,7 @@ Vignette 저장소의 모든 검증 수단(백엔드 단위 테스트, 웹 타
```sh
# apps/api
-python -m pytest app/ -q # 앱 단위 테스트: 2026-08-28 전체 실행 1002 passed
+python -m pytest app/ -q # 앱 단위 테스트: 2026-08-29 전체 실행 1074 passed / 1 skipped
python -m pytest engine_gateway/ -q # 게이트웨이 단위 테스트: 2026-08-28 전체 실행 68 passed
```
@@ -80,12 +80,12 @@ python -m pytest engine_gateway/ --collect-only -q
#### 개선관리 워크북 focused 증거 (2026-08-27~28)
-아래는 전체 1002/68과 별도로 해당 계약을 좁혀 실행한 확정 증거다. 같은 묶음의 테스트가 여러 요구사항
+아래는 전체 API 1074 passed/1 skipped·gateway 68 passed와 별도로 해당 계약을 좁혀 실행한 확정 증거다. 같은 묶음의 테스트가 여러 요구사항
경계를 함께 검증할 수 있으므로 숫자를 요구사항별 전체 합계로 더하지 않는다.
| 항목 | focused 명령/파일 | 확인 결과 |
|---|---|---|
-| C-001 기술 사전검증 | `scripts/check-clinical-crisis-review.py`, `run-clinical-crisis-technical-observations.py`, checker/client reply/source pack/state machine/session focused pytest | 90 passed + Ruff PASS(Starlette 제3자 경고 1건) — P1 생성·스트림 실제 경로 6/6, v2 사례 6건과 런타임 패키지 8개가 HEAD·작업트리 SHA에 일치한다. DB direct load·baseline·carry-over·observed 값을 `1..3`으로 정규화하고 과상한 출력은 재생성하며, 수련생 실제 위기는 엔진 전 중단·109, 내담자 수단 상세는 차단한다. 6시트 개선관리 완료본은 수식 오류 0, 10 완료/1 검토/총 11, canonical gate `pending-valid`다. 사례 30필드·청소년 6답변·자격 포함 승인 10필드·서명 증거가 비어 있어 `review_complete=false`; 외부 임상 승인을 대체하지 않음 |
+| C-001 기술 사전검증 | `scripts/check-clinical-crisis-review.py`, `run-clinical-crisis-technical-observations.py`, checker/client reply/source pack/state machine/session focused pytest | 90 passed + Ruff PASS(Starlette 제3자 경고 1건) — P1 생성·스트림 실제 경로 6/6, v2 사례 6건과 런타임 패키지 8개가 HEAD·작업트리 SHA에 일치한다. DB direct load·baseline·carry-over·observed 값을 `1..3`으로 정규화하고 과상한 출력은 재생성하며, 수련생 실제 위기는 엔진 전 중단·109, 내담자 수단 상세는 차단한다. 6시트 개선관리 완료본은 수식 38개·수식 오류 0, 9 완료/2 검토/총 11, canonical gate `pending-valid`다. REQ-001은 기존 Gmail 인증 세션·관리자 권한·복구 데이터 가시성 수락으로 완료했고, REQ-008 실제 Windows 재부팅 smoke와 C-001 사례 30필드·청소년 6답변·자격 포함 승인 10필드·서명 증거만 남아 있으며 외부 임상 승인을 대체하지 않음 |
| C-002 | `app/test_first_session_checklist.py` | 3 passed — 첫 회기 라포 반영·한 초점 열린 질문, evidence turn, 이후 회기 not-applicable |
| C-003 | `app/test_learner_dashboard.py` | 11 passed — 종료 회기 4건 전에는 insufficient, 이후 dominant share 0.75 경계 |
| REQ-001 | `app/test_auth_providers.py`, `app/test_access_logging.py`, OAuth UI focused E2E, `e2e/admin.spec.ts`/`e2e/uc-admin-console.spec.ts` | auth 41 + access-log redaction 6 pytest, OAuth UI desktop/mobile 4, route-mock browser 2, 실제 DB/browser 1 passed — 모든 provider-verified Google 이메일을 도메인·사전등록 없이 learner·approved로 허용하고 suspended는 보존. callback query는 Uvicorn access log에서 제거. 관리자 사전등록 create는 pending 고정이며 승인 전 `/personas` 403·`/learn→/pending`, 별도 PATCH 승인 뒤 같은 세션 `/personas` 200, `learner_feedback_enabled=false` 영속 재조회, 비활성화·활성 세션 0 |
@@ -99,10 +99,12 @@ python -m pytest engine_gateway/ --collect-only -q
| 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 구조는 유지 |
| 교수자 검토·요약 카드 간격 | `full-sweep-professor.spec.ts --grep "renders six KPI cards"` + `layout-visual-gate.spec.ts` + `session-layout.spec.ts` | 기존 0px에서 회귀 게이트 RED를 확인하고 `--sp-3`=12px로 교정했다. focused 1 passed, 7폭 교수자 gate 1 passed, 전체 layout visual 15 passed, session 무회귀 8 passed. Pages `0c60261e…` 승격 뒤 실제 `/teach`는 computed/실측 12px, overflow 0, console warning/error 0이다. |
-| 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-001 공개 운영 | `https://vignette.chanpaca.net/login`, `/settings`, `/learn/history`, `/admin/continuous-improvement` | 2026-08-28 증거에서 `allowed_email_domains=[]`, Pages `0c60261e…`의 단일 Google CTA·공개 Playwright 2 passed, 기존 인증 세션의 Gmail 로그인 이메일·관리자 콘솔·온보딩 비전환·복구 이력 20건/리뷰 필요 6건을 확인했고 소유자가 데이터 가시성을 수락해 REQ-001을 완료했다. 2026-08-29 public API 530은 별도 현재 장애이며, 이 과거 수락 증거를 current health GREEN으로 재사용하지 않는다. |
+| 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 로그인 이메일·관리자 권한·복구 데이터 가시성을 확인했다. |
| REQ-005·007·008 공개 운영 | P20 회기 `03dddadd-adf3-4922-acbf-31002470da52` | 생성→자기점검 원장 잠금→실제 학습자/AI 내담자 2턴→종료→AI 평가 완료→P20 1회 기록 영속→다음 회기 버튼 재노출 |
-| 운영 배포·복구 | 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는 미실행이다. |
+| 아바타 decode/fallback 후보 | `app/test_upload_storage_contract.py`, `scripts/test_initialize_public_runtime_upload_root.py`, `initialize-public-runtime-upload-root.py probe` | source union 실측은 93 objects·52,973 bytes·inventory SHA-256 `9d703126…e6aa`, decode 정상 3/실패 90이며 현 DB 참조는 정상 2/실패 6/missing 0이다. 후보는 manifest v3 decode 상태·합계·digest를 fail-closed로 결속하고 invalid static 응답을 404/fallback으로 보낸다. focused 단위/probe 증거와 전체 API/Web/PowerShell/E2E·정식 배포 증거를 분리한다. |
+| G8 사람 게이트 현재 상태 | `e2e/continuous-improvement-admin.spec.ts`, `e2e/continuous-improvement-live.spec.ts`, Codex 내장 브라우저 | typecheck와 route-fixture desktop/mobile **10/10** 통과, 3.5초/8초 watchdog overlay 0이다. 격리 local stateful in-app browser에서도 8.5초 뒤 overlay 0·heading 정상, content `keep_quarantine` 뒤 effect 0, 증거 없는 release 승인 disabled→`reject` 뒤 effect 0, 증거 4종 promote 승인 뒤 lifecycle effect 정확히 1을 확인했다. 이 local proof는 GREEN이지만 실DB append-only 결정과 public proof는 아직 남아 있다. |
+| 운영 배포·복구 | deployed API/task baseline `dba9b75a…`·tree `14cd4607…`, Pages `0c60261e-cb37-482d-ba42-d91586194c48` | 2026-08-29 최신 read-only 상태는 local 8001 연결 거부·public API 530·Docker daemon 부재다. 정적 public Web 200과 2026-08-28 green receipt는 현재 API/DB GREEN 증거가 아니다. avatar 후보는 미배포이며, 전체 통합 GREEN·후보 SHA·사용자 승인 전에는 runtime/task/DB/upload-root를 바꾸지 않는다. 실제 Windows 재부팅 smoke도 미실행이다. |
로컬 Playwright는 기존 포트의 서버를 기본 재사용하지 않는다. 공개 preview나 다른 checkout의 stale bundle을
현 작업트리 증거로 오인하지 않도록 충돌 시 fail-closed하며, 동일 dev server를 의도적으로 공유할 때만
@@ -307,7 +309,7 @@ VITE_API_BASE=http://127.0.0.1:8000 npm run e2e # 프록시 대신 API
### 3.6 실측 테스트 개수 (현재)
-2026-08-28 `npx playwright test --list` 기준 **현재 수집 1138 tests / 62 files**다
+2026-08-29 `npx playwright test --list` 기준 **현재 수집 1158 tests / 64 files**다
(유스케이스 16테마 `uc-*.spec.ts` 239 시나리오 포함).
이 숫자는 수집량이지 통과량이 아니다. 현 작업트리 전체 1109개 완주는 아직 증거가 없으며,
과거 전체 GREEN 기록과 이번 focused/release gate 결과를 구분해 적는다.
@@ -553,7 +555,9 @@ 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-28 Gemini 3.7 비용 미산정 회귀 검증: screenshot과 같은 `gemini-3.7-flash-high` 21회·입력 115,950·출력 4,407의 기존 0달러 원장을 2026 프로모션 기준 `≤$0.103489`(`reference_upper_bound`)로 보정한다. Gemini 3.6/3.7 출시·프로모션·2027 경계, 요청별 200K 단가 구간, 일부 미산정 `$…+`, 상한+미산정 `partial_upper_bound`의 `미산정`, 저장된 과거 비용의 라벨 불변, 정확한 `(e2e, fake-client, input=1, output=1, cost=0)` 제외를 회귀화했다. 전체·일별·예산에 비용 확실성을 전파하고 불확실한 평균·비중은 숨긴다. 동일 계량 시그니처 SQL 집계와 migration 18의 client-turn 기간 인덱스를 고정했으며, release agent는 이 파일만 `CREATE INDEX CONCURRENTLY`로 트랜잭션 밖에서 적용한다. focused **105 passed / 1 skipped**, `app engine_gateway` 전체 **1102 passed / 1 skipped**, 새 빈 PostgreSQL에서 온라인 마이그레이션+통합 **1 passed**, release agent **31 passed**, API type generation/check·typecheck·build, 관리자 AI desktop/mobile **6 passed**를 확인했다. 통합 컨테이너와 테스트 DB는 `--rm`으로 정리했으며, 공개 배포·운영 DB 데이터 삭제는 하지 않았다.
- 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-08-29 REQ-001 완료·개선관리 재생성 검증: 기존 실서비스 인증 세션의 `/settings`에서 `yunchan8804@gmail.com`, 관리자 메뉴와 `/admin/continuous-improvement` 접근, 온보딩 비전환을 확인했고 `/learn/history`는 회기 20건·리뷰 필요 6건을 표시했다. 소유자가 데이터 가시성을 수락했으며, 최신 Pages `0c60261e…`의 단일 Google CTA·무제한 도메인 계약·공개 Playwright 2 passed와 결합해 REQ-001을 완료로 승격했다. 완료본은 artifact-tool import→export→reimport로 6시트·11개 고유 요구·38수식·수식 오류 0·완료 9/검토 2를 확인했다. C-001은 패키지 8/8·기술 사례 6/6·canonical checker 정상의 `pending-valid`지만 외부 입력 46칸을 공란으로 보존했고, REQ-008은 실제 Windows 재부팅 smoke 전까지 검토다. 완료본 SHA는 `c832547f…d8bd`, sidecar SHA는 `8c9618f0…a610`이다.
- 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회` 표시를 확인하고 임시 세션을 폐기했다.
diff --git a/docs/ops/backlog-2026-06-26.md b/docs/ops/backlog-2026-06-26.md
index e4b3e17..769ab89 100644
--- a/docs/ops/backlog-2026-06-26.md
+++ b/docs/ops/backlog-2026-06-26.md
@@ -12,13 +12,19 @@
> 섹션과 상세 근거 `docs/ops/source-docs-gap-analysis-2026-06-26.md`에서 추적한다. C1~C3/H·M 구조는 코드로
> 선제 구축했고, 임상 문안·평가기준·골든셋 콘텐츠는 임상팀(구훈정·어유경) 외부 정의로 받는다.
>
-> **개선관리 워크북(2026-08-29)** — C-002·C-003과 REQ-002~007은 내부 기술·요구 실증 DONE이다. REQ-001은
-> single Google CTA를 Pages production `ef48c0ae…`에 승격했지만 실제 Gmail callback, REQ-008은 recovery task
-> `dba9b75a…`·tree `14cd4607…`와 5174 자동복구까지 완료했지만 실제 Windows 재부팅 smoke가 남아 둘 다 `검토`다. C-001은
+> **개선관리 워크북(2026-08-29)** — C-002·C-003과 REQ-001~007은 완료다. REQ-001은 기존 Gmail 인증 세션의
+> 로그인 이메일·관리자 권한·온보딩 비전환·복구 데이터 가시성을 소유자가 수락해 완료로 승격했다. REQ-008은 recovery task
+> 계약과 5174 자동복구까지 완료했지만 실제 Windows 재부팅 smoke가 남아 `검토`다. C-001은
> v2 합성 사례 6건·상태별 canonical 판정기와 자동
> 안전 게이트의 기술 사전검증까지 완료됐다. 사례별 5필드 30개·청소년 답변 6개·검토자 이름/소속/자격을 포함한
> 승인 10필드와 서명 증거가 없어 B4 외부 GATE로 유지하며, `review_complete=true` 전에는 완료로 닫지 않는다.
-> 현재 개선관리 집계는 완료 8·검토 3·총 11이다.
+> 현재 개선관리 집계는 **완료 9·검토 2·총 11**이다. C-001 외부 입력 46칸은 조작하지 않는다.
+>
+> **2026-08-29 최신 운영 상태** — 정적 public Web은 200이지만 local API 8001은 연결 거부, public API는 530이고
+> Docker daemon 부재로 DB/API는 OUTAGE다. 배포된 동적 runtime/task 기준선은 old `dba9b75a…`, Pages는
+> `0c60261e…`(source `5bf89ff…`)다. avatar union은 93 objects·52,973 bytes·SHA-256 `9d703126…e6aa`, decode
+> 정상 3/실패 90, 현 DB 참조 정상 2/실패 6/missing 0이다. fail-closed 후보와 deployed baseline을 분리하며,
+> 전체 회귀·아바타 실브라우저 fallback·G8 실DB/public 증거·사용자 배포 승인 전에는 운영 전체를 GREEN으로 올리지 않는다.
>
> **Outcome & Alliance OS** — 2026-08-06 정식 전략 실행 트랙으로 승격했다. G0 Measurement Truth, G1
> 현재 소스·실행 증거 재감사에서는 G0~G6과 G8이 DONE이다. G1 승격 prompt 1.2+read-skew/JSON 복구는 24/24 ready·방향 9/9·오류 0을 재확인했고, G0 census 29/29·위반 0, G4/G5 실제 API/DB/브라우저 폐루프, G6 safety metadata-only 최우선 runtime을 disposable clone에서 확인했다. G8은 실제 receipt-bound image rollback 2회(`nas-g8-723eeef2…`/`nas-g8-2738846c…`)에 더해 source HEAD `61a41d1f…6af`·tree `87dec55d…3b77`·archive `4d15d055…119d4d`의 candidate 112/112와 실제 NAS 평문 origin 112/112를 통과했다. 과거 `6030a677…c611`의 UUID 24건 실패와 후속 SHA 결함 rollback은 이력으로 보존하며 현재 완료 증거로 재사용하지 않는다. G7 Multimodal Alliance는 내부 구현 DONE과 외부 proof GATE를 분리한다. detached-clean public `a73bcd24…`·OpenAPI 126·`local_whisper`/`melotts` ready·authenticated WSS 무마이크 rehearsal까지 완료했고, 명시 동의 물리 마이크 3,120초·독립 라벨 voice-gain benchmark·동시 topology high-water를 추적한다. 외부 Deepgram/OpenAI adapter는 fallback으로 보존한다. G0~G8과
@@ -40,6 +46,17 @@
## B2. 환경 제약 — 이 워크스테이션에서 증거 생산 불가
+- [ ] **public API/DB·아바타 정식 승격** — Docker/API 장애를 복구하고, 손상 아바타는 private forensic으로 보존하되
+ public 404/fallback, decode-valid 객체만 immutable cache에 제공하는 후보를 전체 API/Web/PowerShell·내장 브라우저 E2E로
+ 검증한다. exact 93 objects·52,973 bytes·`9d703126…e6aa` receipt와 후보 commit SHA를 고정한 뒤 push/Pages/runtime/task/
+ upload-root 범위를 명시 승인받는다. 신규 UUID 업로드의 URL 기록·현재 참조·GET/HEAD도 full decode하며 사후 손상은 current
+ invalid/health fallback과 404로 닫는 후보까지 API 1074/1 skip을 통과했다. Docker engine OFF + 활성 public watchdog 때문에
+ 풀스택은 watchdog 일시 비활성화와 고유 56432/58000/55173 격리 자원·exact cleanup을 승인받아야 한다. DB avatar URL 삭제·
+ 파일 초기화·task/runtime 재시작을 선행하지 않는다.
+- [ ] **G8 사람 게이트 실DB/public 실증** — 승인·보류(`keep_quarantine`)·반려(`reject`)와 사유 입력, 증거 미충족 승인
+ 차단은 typecheck와 route-fixture desktop/mobile 10/10을 통과했다. Codex 내장 브라우저의 격리 local stateful fixture도
+ 8.5초 overlay 0·heading 정상, content 보류와 증거 없는 release 반려 effect 0, 증거 4종 promote 승인 lifecycle effect 1로
+ GREEN이다. 남은 것은 사용자 행동 시점 확인 뒤 실행할 실DB append-only 결정과 public proof다. local fixture는 이를 대신하지 않는다.
- [ ] **공개 DB 계정·회기 복구 안정화** — 2026-08-07 18:26 KST owner 승인으로 recovered named volume을
`vignette-dev-db:55432`에 전환했다. cutover owner 집계 users 84, sessions 30, turns 705, Google 계정 16,
Google 소유 회기 30, orphan 0이며 health·engine·OAuth·watchdog가 정상이다. 기존 인증 세션은 복사하지
diff --git a/docs/ops/evidence/avatar-decode-audit-2026-08-29.json b/docs/ops/evidence/avatar-decode-audit-2026-08-29.json
new file mode 100644
index 0000000..469ab7b
--- /dev/null
+++ b/docs/ops/evidence/avatar-decode-audit-2026-08-29.json
@@ -0,0 +1,73 @@
+{
+ "schema_version": "vignette.avatar-decode-audit.v1",
+ "generated_at": "2026-08-29T21:18:49+09:00",
+ "status": "deployment_blocked",
+ "method": {
+ "runtime": "Python 3.11",
+ "decoder": "Pillow 12.2.0 Image.verify",
+ "extension_contract": [
+ "png",
+ "jpg",
+ "jpeg",
+ "webp"
+ ],
+ "privacy": {
+ "raw_filenames_recorded": false,
+ "raw_paths_recorded": false,
+ "user_ids_recorded": false,
+ "emails_recorded": false,
+ "avatar_urls_recorded": false
+ }
+ },
+ "source_union": {
+ "source_root_count": 3,
+ "object_count": 93,
+ "inventory_sha256": "9d703126f78d4fc8330408835d76a7d680276240dc578d6fc9ca420c2f25e6aa",
+ "total_size_bytes": 52973,
+ "maximum_object_size_bytes": 28208,
+ "same_name_content_conflicts": 0,
+ "missing_database_references": 0,
+ "decode_valid_count": 3,
+ "decode_invalid_count": 90,
+ "extension_mismatch_count": 0,
+ "valid_formats": {
+ "JPEG": 2,
+ "PNG": 1
+ },
+ "valid_dimension_counts": {
+ "1x1": 1,
+ "225x225": 1,
+ "512x512": 1
+ }
+ },
+ "connected_database": {
+ "database_target_sha256": "81fe4a2844b7340f21396931fa18580e24358f857a08cc60540ddf8a4f8789b5",
+ "reference_count": 8,
+ "unique_object_count": 8,
+ "reference_set_sha256": "70926cf36ceb2375dd6c471bc59a38138460d8e4895ad1f2bddbcf1a49210d2b",
+ "active_private_audio_count": 0,
+ "found_object_count": 8,
+ "missing_object_count": 0,
+ "decode_valid_count": 2,
+ "decode_invalid_count": 6,
+ "valid_formats": {
+ "JPEG": 2
+ },
+ "valid_dimension_counts": {
+ "225x225": 1,
+ "512x512": 1
+ },
+ "invalid_signature_counts": {
+ "PNG": 6
+ },
+ "invalid_size_counts": {
+ "70": 6
+ },
+ "invalid_unique_content_count": 1,
+ "all_invalid_objects_have_identical_content": true
+ },
+ "release_gate": {
+ "allowed": false,
+ "reason": "Six of eight database-referenced avatar objects have a PNG signature but fail full image decoding. Preserve the bytes and database references until the owner chooses a recovery policy; do not claim the broken-image incident is resolved or deploy the current storage candidate."
+ }
+}
diff --git a/docs/ops/evidence/avatar-image-resilience-recovery-2026-08-29.json b/docs/ops/evidence/avatar-image-resilience-recovery-2026-08-29.json
new file mode 100644
index 0000000..0fce29c
--- /dev/null
+++ b/docs/ops/evidence/avatar-image-resilience-recovery-2026-08-29.json
@@ -0,0 +1,134 @@
+{
+ "schema": "vignette.avatar-image-resilience-recovery.v1",
+ "evidence_id": "AVATAR-IMAGE-RESILIENCE-RECOVERY-2026-08-29",
+ "captured_at": "2026-08-29T18:00:42+09:00",
+ "privacy": {
+ "raw_user_uuid_recorded": false,
+ "raw_filename_recorded": false,
+ "email_recorded": false,
+ "resource_identity": "SHA-256 식별자만 기록"
+ },
+ "incident": {
+ "root_cause": {
+ "runtime_mode": "Python uvicorn 직접 실행 프로세스",
+ "process_working_directory": "/apps/api",
+ "user_upload_dir_environment": "미설정",
+ "effective_user_upload_dir": "cwd 기준 상대경로 uploads",
+ "effective_storage_location": "/apps/api/uploads",
+ "docker_named_upload_volume_used_by_public_api": false,
+ "failure_mechanism": "DB 경로는 승격 뒤에도 남았지만 업로드 파일은 이전 detached release root에 머물렀다. 새로 승격된 Python 직접 실행 런타임이 같은 상대 업로드 경로를 서로 다른 작업 디렉터리에서 해석해 파일을 찾지 못했다."
+ },
+ "initial_public_failure": {
+ "phase": "복구 전",
+ "http_status": 404,
+ "response_content_type": "application/json",
+ "browser_img_complete": true,
+ "browser_img_natural_width": 0,
+ "browser_img_natural_height": 0
+ },
+ "problem_object": {
+ "path_sha256": "484dcee08b007741872ac48b49421fd45bf5a60b273e7a64c5b9a66dac85138a",
+ "bytes": 28208,
+ "content_sha256": "c5d06061bfa635d65f8285df242c5c9820dadcffcd785ed3f9b8619be24212c7"
+ }
+ },
+ "recovery": {
+ "source_release_commit_prefix": "bf5f7352cee0",
+ "current_release_commit_prefix": "dba9b75a3887",
+ "objects": [
+ {
+ "object_key": "recovered_object_1",
+ "bytes": 18395,
+ "content_sha256": "854403aed367db2d459e482a5e980fd22a846738b6034068cd7b4e80bbb7f34d"
+ },
+ {
+ "object_key": "problem_object",
+ "bytes": 28208,
+ "content_sha256": "c5d06061bfa635d65f8285df242c5c9820dadcffcd785ed3f9b8619be24212c7"
+ }
+ ],
+ "object_count": 2,
+ "total_bytes": 46603,
+ "source_and_current_content_hashes_match": true,
+ "current_release_storage_path_kind": "release root 내부의 일반 디렉터리",
+ "current_release_git_dirty_paths": 0,
+ "final_public_probe": {
+ "http_status": 200,
+ "content_type": "image/jpeg",
+ "bytes": 28208,
+ "content_sha256": "c5d06061bfa635d65f8285df242c5c9820dadcffcd785ed3f9b8619be24212c7",
+ "matches_recovered_problem_object": true
+ },
+ "authenticated_browser_dom": {
+ "route": "/admin/users",
+ "image_complete": true,
+ "natural_width": 512,
+ "natural_height": 512,
+ "computed_display": "block",
+ "visible": true,
+ "raw_source_recorded": false
+ }
+ },
+ "ui_resilience": {
+ "files": [
+ "apps/web/src/components/avatar/ResilientImage.tsx",
+ "apps/web/src/components/shell/Topbar.tsx",
+ "apps/web/src/pages/Settings.tsx",
+ "apps/web/src/pages/Onboarding.tsx",
+ "apps/web/src/lib/auth.tsx",
+ "apps/web/e2e/image-resilience.spec.ts",
+ "apps/web/e2e/uc-settings-consent.spec.ts"
+ ],
+ "covered_states": [
+ "유효한 외부 HTTPS 이미지",
+ "API 상대경로 이미지",
+ "빈 소스",
+ "허용하지 않는 비웹 scheme",
+ "HTTP 404",
+ "디코딩 또는 로드 실패",
+ "지연 응답 중 0x0 비노출 로딩 상태",
+ "업로드 뒤 소스 교체",
+ "접근 가능한 이니셜 fallback"
+ ],
+ "verification": {
+ "typecheck": {
+ "command": "npm run typecheck",
+ "result": "passed"
+ },
+ "production_build": {
+ "command": "npm run build",
+ "result": "passed",
+ "vite_modules_transformed": 148,
+ "deterministic_runs": 2,
+ "dist_file_count": 601,
+ "dist_manifest_sha256": "35c48fbf844efd0514282b76937bbef3b1f173992313c7d377363d71f0806d89"
+ },
+ "design_ssot": {
+ "command": "npm run check:design-ssot",
+ "result": "passed"
+ },
+ "focused_e2e": {
+ "command": "npx playwright test e2e/image-resilience.spec.ts e2e/uc-settings-consent.spec.ts --project=chromium-desktop --project=chromium-mobile",
+ "result": "40/40 passed"
+ },
+ "layout_and_tabs_e2e": "17/17 passed",
+ "session_layout_e2e": "8/8 passed",
+ "scoped_diff_check": "passed"
+ },
+ "release_candidate": {
+ "base_commit": "5bf89ff4ee971940882695d27f153e1460df295e",
+ "commit": "35a62fda90d3ba56707acfeb3fb94090d3949f8e",
+ "tree": "d8634141289b6a0007954671ece5fb7dba9c2881",
+ "author": "Yun Chan",
+ "clean": true,
+ "protected_api_or_generated_paths_changed": 0,
+ "remote_push_and_pages_deploy": "pending_explicit_external_write_approval"
+ }
+ },
+ "stable_upload_contract": {
+ "status": "pending",
+ "current_recovery_scope": "현재 release root에 대한 시점 복구",
+ "remaining_risk": "USER_UPLOAD_DIR가 cwd 상대경로인 동안 이후 승격이나 다른 release root 재시작에서 업로드가 다시 고립될 수 있다.",
+ "required_follow_up": "사용자 업로드를 release root 밖의 절대 영속 상태 디렉터리로 옮기고, 모든 부팅·watchdog 진입점에서 USER_UPLOAD_DIR를 주입·검증한 뒤 inventory, 콘텐츠 해시 검증, 중복 제거, rollback, 공개 재시작 증거를 포함해 마이그레이션한다."
+ }
+}
diff --git a/docs/ops/evidence/google-account-live-browser-proof-2026-08-29.json b/docs/ops/evidence/google-account-live-browser-proof-2026-08-29.json
new file mode 100644
index 0000000..5d41d4d
--- /dev/null
+++ b/docs/ops/evidence/google-account-live-browser-proof-2026-08-29.json
@@ -0,0 +1,63 @@
+{
+ "schema_version": 1,
+ "status": "verified",
+ "captured_at_utc": "2026-08-29T08:09:03.6788264Z",
+ "scope": "REQ-001 live Google account, administrator entitlement and restored-data acceptance",
+ "current_session": {
+ "frontend_origin": "https://vignette.chanpaca.net",
+ "settings_route": "/settings",
+ "login_email": "yunchan8804@gmail.com",
+ "display_name": "윤찬",
+ "admin_navigation_present": true,
+ "admin_continuous_improvement_route_accessible": true,
+ "onboarding_redirect_absent": true,
+ "observation_mode": "read-only existing authenticated browser session"
+ },
+ "restored_data": {
+ "learner_history_route": "/learn/history",
+ "visible_history_count": 20,
+ "visible_review_required_count": 6,
+ "canonical_database_counts_from_recovery_ledger": {
+ "sessions": 21,
+ "turns": 64,
+ "evaluations": 6
+ },
+ "recovery_ledger": "docs/ops/evidence/google-account-data-recovery-2026-08-29.json",
+ "owner_confirmed_data_visibility": true
+ },
+ "public_login": {
+ "production_deployment_id": "0c60261e-cb37-482d-ba42-d91586194c48",
+ "source_commit": "5bf89ff4ee971940882695d27f153e1460df295e",
+ "source_tree": "29f76aeb37d911a6947f96020719826f606f9c58",
+ "single_google_cta_count": 1,
+ "legacy_google_selector_count": 0,
+ "dev_login_count": 0,
+ "cta_visible": true,
+ "cta_enabled": true,
+ "cta_size_px": {
+ "width": 364,
+ "height": 60
+ },
+ "horizontal_overflow_px": 0,
+ "auth_config_status": 200,
+ "google_enabled": true,
+ "allowed_email_domains": [],
+ "dev_login_enabled": false,
+ "health_status": 200,
+ "health_environment": "prod",
+ "health_db": true,
+ "health_engine": true,
+ "public_auth_playwright": "1 passed",
+ "login_policy_playwright": "1 passed"
+ },
+ "acceptance": {
+ "requirement": "REQ-001",
+ "status": "complete",
+ "basis": "The existing live session identifies yunchan8804@gmail.com, exposes the administrator console without onboarding, shows restored history, and the owner confirmed that the recovered data is visible. The current production login surface independently keeps the unrestricted single Google CTA contract."
+ },
+ "safety": {
+ "agent_started_new_oauth": false,
+ "agent_selected_google_account": false,
+ "agent_changed_account_or_data": false
+ }
+}
diff --git a/docs/ops/evidence/req008-reboot-preflight-2026-08-29.json b/docs/ops/evidence/req008-reboot-preflight-2026-08-29.json
new file mode 100644
index 0000000..5397234
--- /dev/null
+++ b/docs/ops/evidence/req008-reboot-preflight-2026-08-29.json
@@ -0,0 +1,82 @@
+{
+ "evidence_id": "REQ008-REBOOT-PREFLIGHT-2026-08-29",
+ "captured_at": "2026-08-29T17:40:03+09:00",
+ "mode": "read-only-preflight",
+ "status": "go_pending_explicit_reboot_approval",
+ "mutations_performed": [],
+ "release_pin": {
+ "root": "D:\\workspace\\vignette-public-runtime-dba9b75a3887",
+ "commit": "dba9b75a388783b11d821257e2f91fbcc30bbe1a",
+ "tree": "14cd4607b07f93e06cef8c912694ae1da8517b8c",
+ "detached_head": true,
+ "dirty_paths": 0,
+ "boot_script_sha256": "04dd9144a3943822d0f794e2973f59299edc7595bc2d85919275a07fa5225a54",
+ "watchdog_script_sha256": "904b4751ecf1b1db5e41dc7b85ad2f88e7c6a55a3349db6bfa8988ad9babfc56",
+ "start_script_sha256": "67d2a41ac4a366888b324e40e1c77b75e045505b4bd888c72c3a476a2c1f5c05"
+ },
+ "scheduled_tasks": {
+ "account": "encep",
+ "logon_type": "Interactive",
+ "run_level": "Limited",
+ "boot_task": {
+ "state": "Ready",
+ "last_result": 0,
+ "current_pin_run_proven": false,
+ "reason": "마지막 실행은 2026-08-28 18:14이고 현재 release root는 2026-08-29 15:36에 생성되어 실제 재부팅 검증이 남아 있다."
+ },
+ "watchdog_task": {
+ "state": "Ready",
+ "last_run": "2026-08-29T17:33:54+09:00",
+ "last_result": 0,
+ "failcount": 0
+ }
+ },
+ "current_probes": {
+ "listeners": [55432, 8001, 5174, 9099, 9882, 9883],
+ "components_healthy": [
+ "db",
+ "api",
+ "engine",
+ "web-preview",
+ "voice-api",
+ "voice-sidecars",
+ "cloudflared",
+ "public-api"
+ ],
+ "local_api_http": 200,
+ "public_api_http": 200,
+ "public_login_http": 200,
+ "public_unauthenticated_me_http": 401,
+ "environment": "prod",
+ "db": true,
+ "engine": true,
+ "voice": {
+ "stt_provider": "local_whisper",
+ "stt_model": "small",
+ "tts_provider": "melotts",
+ "tts_model": "melotts-korean"
+ },
+ "google_auth_enabled": true,
+ "allowed_domains": [],
+ "dev_login_enabled": false
+ },
+ "windows_reboot_state": {
+ "cbs_reboot_required": false,
+ "windows_update_reboot_required": false,
+ "pending_file_rename_operation_strings": 74,
+ "interpretation": "대기 중인 rename 또는 delete 쌍이 다음 재부팅에 적용될 수 있다는 뜻이며 장애나 파일 74개의 증거는 아니다."
+ },
+ "remaining_gate": {
+ "requires_user_confirmation": true,
+ "confirmation_scope": "저장하지 않은 작업이 없고 지금 Windows를 재부팅해도 되는지",
+ "required_login": "encep",
+ "pass_conditions": [
+ "이번 부팅 이후 boot와 watchdog task가 각각 result 0",
+ "새 boot-public-runtime.log에 boot OK",
+ "watchdog failcount 0",
+ "현재 release pin의 CheckOnly가 8개 구성요소 모두 healthy",
+ "local 및 public API, engine, web가 모두 200",
+ "prod, db=true, engine=true와 voice provider/model 계약 유지"
+ ]
+ }
+}
diff --git a/docs/ops/evidence/workbook-req001-completion-sync-2026-08-29.json b/docs/ops/evidence/workbook-req001-completion-sync-2026-08-29.json
new file mode 100644
index 0000000..04fd645
--- /dev/null
+++ b/docs/ops/evidence/workbook-req001-completion-sync-2026-08-29.json
@@ -0,0 +1,93 @@
+{
+ "schema_version": 1,
+ "status": "review_gates_remain",
+ "captured_at_utc": "2026-08-29T08:24:45.2582699Z",
+ "git_head": "ac9b7026881139780938f4c4f2b89a235b0a0c08",
+ "workbook": {
+ "path": "outputs/01a04217-f73b-7303-b597-401fa7f5d290/Vignette_개선관리_완료.xlsx",
+ "previous_sha256": "362afd218aa9dd67c22cccd5125b8efb18cfaff06945af1ce550e868a3fce39f",
+ "sha256": "c832547f30ae0664e54b302c8e9f62cc157f31022ad158d17803d8cac988d8bd",
+ "bytes": 2751365,
+ "sheet_count": 6,
+ "requirement_count": 11,
+ "formula_count": 38,
+ "formula_error_count": 0,
+ "summary": {
+ "complete": 9,
+ "review": 2,
+ "hold": 0,
+ "total": 11
+ }
+ },
+ "sidecar": {
+ "path": "outputs/01a04217-f73b-7303-b597-401fa7f5d290/Vignette_개선관리_완료.xlsx.inspect.ndjson",
+ "sha256": "8c9618f0931219449af9ed127faf886c53f31823eea613671184204de44ba610",
+ "bytes": 313115,
+ "kind_counts": {
+ "workbook": 1,
+ "sheet": 6,
+ "table": 6,
+ "region": 9,
+ "formula": 38,
+ "computedStyle": 436,
+ "drawing": 4,
+ "conditionalFormatting": 8
+ }
+ },
+ "status_changes": {
+ "REQ-001": {
+ "from": "검토",
+ "to": "완료",
+ "evidence": "docs/ops/evidence/google-account-live-browser-proof-2026-08-29.json",
+ "basis": "The existing authenticated production session identifies yunchan8804@gmail.com, preserves administrator access without onboarding, exposes restored history, and the owner accepted the recovered data visibility. The current production login surface independently keeps one unrestricted Google CTA."
+ }
+ },
+ "c001": {
+ "improvement_status": "검토",
+ "technical_status": "verified",
+ "clinical_status": "pending_external_review",
+ "machine_gate": "pending-valid",
+ "package_match": "PACKAGE_MATCH",
+ "canonical_checker": {
+ "ok": true,
+ "review_complete": false,
+ "external_review_replaced": false
+ },
+ "external_input_cells_preserved_empty": 46,
+ "external_review_fabricated": false
+ },
+ "builder_hardening": {
+ "canonical_checker_transport": "The checker result is passed explicitly to the workbook builder because nested child-process creation is denied in the managed runtime. Missing or malformed input remains fail-closed.",
+ "named_gate_predicates": true,
+ "failed_predicates_rendered_when_present": true,
+ "post_export_assertions": [
+ "C-001 machine gate",
+ "C-001 package match",
+ "C-001 clinical status",
+ "C-001 final review status",
+ "46 external input cells remain empty",
+ "38 formulas",
+ "summary 9 complete, 2 review, 0 hold, 11 total",
+ "REQ-001 complete"
+ ]
+ },
+ "verification": {
+ "artifact_tool_import_export_reimport": "passed",
+ "structured_extraction": "6 sheets, 11 unique requirements, 38 formulas",
+ "formula_error_scan": "0 matches",
+ "all_sheet_visual_pass": 6,
+ "focused_c001_visual_pass": 4,
+ "render_count": 10,
+ "render_set_byte_identical_to_visually_checked_candidate": true,
+ "stale_deployment_ids_found": 0,
+ "public_pages_deployment_id": "0c60261e-cb37-482d-ba42-d91586194c48",
+ "public_pages_source": "5bf89ff4ee971940882695d27f153e1460df295e",
+ "public_api_commit": "dba9b75a388783b11d821257e2f91fbcc30bbe1a",
+ "public_api_tree": "14cd4607b07f93e06cef8c912694ae1da8517b8c"
+ },
+ "remaining_gates": {
+ "C-001": "A real qualified external clinical reviewer must complete the six case verdict blocks, six youth-specific answers, ten approval fields and signed evidence. This evidence cannot be fabricated or self-approved.",
+ "REQ-008": "An actual Windows restart and post-boot API, engine, tunnel and browser recovery smoke are still required."
+ },
+ "historical_evidence_mutated": false
+}
diff --git a/docs/ops/evidence/workbook-runtime-sync-2026-08-29.json b/docs/ops/evidence/workbook-runtime-sync-2026-08-29.json
new file mode 100644
index 0000000..71991cf
--- /dev/null
+++ b/docs/ops/evidence/workbook-runtime-sync-2026-08-29.json
@@ -0,0 +1,59 @@
+{
+ "schema_version": 1,
+ "status": "review_gates_remain",
+ "captured_at_utc": "2026-08-29T07:19:40.531Z",
+ "git_head": "ffadc8bd49ca41ade5b84590fe7effc53c7b6a0b",
+ "workbook": {
+ "path": "outputs/01a04217-f73b-7303-b597-401fa7f5d290/Vignette_개선관리_완료.xlsx",
+ "previous_sha256": "0a64d1bcdfd0c1727ebd4279b58b3077227ecba2e30d68009cf9ba8d310b1177",
+ "sha256": "aab6ef52c388e3112be4c88f6fb21b9bceb9ae0604e099549fbd2b7209b4e26a",
+ "bytes": 2751636,
+ "sheet_count": 6,
+ "formula_error_count": 0,
+ "summary": {
+ "complete": 8,
+ "review": 3,
+ "total": 11
+ }
+ },
+ "edit_scope": {
+ "values_only": true,
+ "formats_preserved": true,
+ "formulas_changed": false,
+ "statuses_changed": false,
+ "ranges": [
+ "개발·기능 이슈!M5:M12",
+ "임상·교육 이슈!M6:M7"
+ ],
+ "purpose": "Replace stale deployment references with the current API dba9b75a and Pages ef48c0ae evidence while preserving unresolved review gates"
+ },
+ "verification": {
+ "artifact_tool_import_export_reimport": "passed",
+ "all_sheet_visual_pass": 6,
+ "post_edit_focused_visual_pass": 4,
+ "clinical_checker": "passed; pending_external_review; review_complete=false",
+ "public_pages_deployment_id": "ef48c0ae-374e-41fc-b6fd-03f69b26e946",
+ "public_auth_e2e": "1 passed",
+ "public_api_commit": "dba9b75a388783b11d821257e2f91fbcc30bbe1a",
+ "public_api_tree": "14cd4607b07f93e06cef8c912694ae1da8517b8c"
+ },
+ "c001_artifact_continuity": {
+ "historical_preflight_path": "docs/ops/evidence/c-001-clinical-technical-preflight-2026-08-28.json",
+ "historical_preflight_artifact_count": 14,
+ "historical_preflight_current_match_count": 13,
+ "technical_artifact_match_count": 13,
+ "technical_artifact_total": 13,
+ "historical_workbook_sha256": "0a64d1bcdfd0c1727ebd4279b58b3077227ecba2e30d68009cf9ba8d310b1177",
+ "current_workbook_sha256": "aab6ef52c388e3112be4c88f6fb21b9bceb9ae0604e099549fbd2b7209b4e26a",
+ "current_package_match_count": 14,
+ "current_package_artifact_count": 14,
+ "continuity_rule": "13 unchanged technical artifacts from the immutable historical preflight plus the current workbook hash recorded by this evidence",
+ "historical_record_mutated": false,
+ "external_review_state_changed": false
+ },
+ "remaining_gates": {
+ "C-001": "Real external clinical reviewer verdicts, reviewer identity and qualifications, youth-specific answers, approval decision and signed evidence are absent",
+ "REQ-001": "Live yunchan8804@gmail.com OAuth callback and restored admin/data browser proof are absent",
+ "REQ-008": "Actual Windows reboot and post-boot API/engine/tunnel/browser recovery proof are absent"
+ }
+}
diff --git a/docs/ops/handoff-goal-production-2026-08-29.md b/docs/ops/handoff-goal-production-2026-08-29.md
new file mode 100644
index 0000000..ae46c86
--- /dev/null
+++ b/docs/ops/handoff-goal-production-2026-08-29.md
@@ -0,0 +1,350 @@
+# 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 `, 한글의 짧은 커밋 메시지를 사용한다.
+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일 때 사용자에게 아래 범위를 정확히 제시하고 승인받는다.
+
+> 후보 커밋 ``를 원격에 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와 승인 범위를 먼저 보여줘.
diff --git a/docs/ops/public-runtime-watchdog.md b/docs/ops/public-runtime-watchdog.md
index 65abcdb..e18a8a4 100644
--- a/docs/ops/public-runtime-watchdog.md
+++ b/docs/ops/public-runtime-watchdog.md
@@ -24,6 +24,20 @@ watchdog은 이 증거를 health probe와 failcount 기록보다 먼저 다시
API secret은 release root의 apps/api/.env에 두되 task 인자에는 넣지 않는다. 이 파일과 web node_modules,
runtime log는 Git ignore 대상이다. Cloudflared와 Claude CLI credential은 현재 Windows 사용자 profile에 둔다.
+사용자 업로드는 release root와 분리한 영속 절대 경로만 사용한다. 권장 기본값은
+`%LOCALAPPDATA%\Vignette\public-runtime\uploads`다. 이 경로는 같은 Windows 호스트의 release 교체와 재부팅에는
+유지되지만 호스트 장애를 견디는 외부 백업은 아니다. consumer(start/boot/watchdog/registrar)는 빈 경로를 만들지
+않는다. 별도 initializer가 현재 DB의 정확한 `/uploads/profile-avatars/` 참조를 copy-only·no-overwrite·SHA-256으로
+검증해 만든 뒤에만 두 task action에 `-UserUploadDir`로 고정한다. 상대 경로, Git root와 겹치거나 이를 포함하는 경로, 기존
+symlink/junction/reparse point를 통과하는 경로, 디렉터리가 아니거나 쓸 수 없는 경로는 프로세스 변경 전에
+fail-closed한다. watchdog `-CheckOnly`는 실행 중 API 프로세스의 `USER_UPLOAD_DIR`까지 비교하므로, health가
+정상이더라도 값이 없거나 다른 release-local 경로면 실패한다.
+
+공개 static mount는 `USER_UPLOAD_DIR/profile-avatars` 하나뿐이다. 같은 legacy root의 `multimodal-audio`는 private
+storage이며 공개 migration 대상도 static 서빙 대상도 아니다. 보존 중인 private audio DB 참조가 하나라도 있으면
+initializer는 별도 private migration 없이는 중단한다. migration manifest와 write-freeze sentinel은 공개 upload root와
+Git root 밖의 절대 private state directory에만 둔다.
+
## Stable Release 준비
아래 작업은 승인된 clean commit이 생긴 뒤 단일 public mutation owner가 수행한다. 기존 release root를
@@ -34,6 +48,7 @@ runtime log는 Git ignore 대상이다. Cloudflared와 Claude CLI credential은
$commit = (& git.exe -C $repoRoot rev-parse --verify HEAD).Trim()
if ($LASTEXITCODE -ne 0) { throw 'HEAD 조회 실패' }
$releaseRoot = "D:\workspace\vignette-public-runtime-$($commit.Substring(0, 12))"
+ $userUploadDir = Join-Path $env:LOCALAPPDATA 'Vignette\public-runtime\uploads'
if (Test-Path -LiteralPath $releaseRoot) { throw "release root already exists: $releaseRoot" }
& git.exe -C $repoRoot worktree add --detach $releaseRoot $commit
@@ -54,6 +69,48 @@ runtime log는 Git ignore 대상이다. Cloudflared와 Claude CLI credential은
apps/api/.env의 내용을 console이나 evidence에 출력하지 않는다. 새 root에 node_modules와 .env를 준비한 뒤에도
위 Git status 결과는 빈 값이어야 한다.
+## 공개 아바타 저장소 초기화와 fresh cutover
+
+이 단계가 task 설치보다 먼저다. 기존 API도 `upload_write_freeze` health 계약을 지원해야 한다. initializer는 freeze를
+`CreateNew`로 게시하고 기존 API의 write lease가 0이 될 때까지 기다린 뒤, caller가 명시한 3개 source root의 flat
+`profile-avatars` regular file 전체 union을 보존한다. 현재 승인 기준은 preserved 93개와 DB 참조 8개이며, 같은 URL을
+여러 행이 참조하면 파일은 한 번 복사하고 reference count는 보존한다. 호출자는 사전 계산한 preserved object count와
+privacy-safe path/content/size inventory SHA256을 함께 고정해야 한다. worker는 copy 전후 재스캔과 manifest v2 proof까지
+그 pin을 재검증한다. 원본은 삭제·이동하지 않으며 대상 충돌, source 간 hash 충돌, 누락 1건, 경로 인코딩/중첩,
+active private audio가 있으면 중단한다.
+
+ $userUploadDir = Join-Path $env:LOCALAPPDATA 'Vignette\public-runtime\uploads'
+ $uploadStateDir = Join-Path $env:LOCALAPPDATA 'Vignette\public-runtime\private-state'
+ $uploadFreezePath = Join-Path $uploadStateDir 'avatar-cutover.freeze.json'
+ $legacyUploadRoots = @(
+ 'D:\exact-approved-upload-root-1'
+ 'D:\exact-approved-upload-root-2'
+ 'D:\exact-approved-upload-root-3'
+ )
+ $expectedPreservedInventorySha256 = ''
+ $initializer = Join-Path $releaseRoot 'scripts\initialize-public-runtime-upload-root.ps1'
+ $initJson = & powershell.exe -NoProfile -ExecutionPolicy Bypass -File $initializer `
+ -StableSourceRoot $releaseRoot `
+ -UserUploadDir $userUploadDir `
+ -ManifestStateDir $uploadStateDir `
+ -UserUploadWriteFreezePath $uploadFreezePath `
+ -ExpectedReferenceCount 8 `
+ -ExpectedPreservedObjectCount 93 `
+ -ExpectedPreservedInventorySha256 $expectedPreservedInventorySha256 `
+ -SourceUploadDir $legacyUploadRoots
+ if ($LASTEXITCODE -ne 0) { throw 'avatar storage initialization failed' }
+ $init = $initJson | ConvertFrom-Json
+ $uploadManifestSha = [string]$init.manifest_sha256
+ $uploadManifestPath = Join-Path $uploadStateDir "public-avatar-upload-$uploadManifestSha.json"
+ if ((Get-FileHash -LiteralPath $uploadManifestPath -Algorithm SHA256).Hash.ToLowerInvariant() -ne $uploadManifestSha) {
+ throw 'private migration manifest hash mismatch'
+ }
+
+initializer 성공 시 freeze는 의도적으로 남는다. 이어지는 `-RequireFreshPublicProvenance` cutover는 old API가
+active+valid+drained freeze를 증명한 뒤 tunnel을 먼저 닫고 API를 교체한다. frozen 새 API와 새 tunnel의 local/public GET,
+listener PID/cwd/env, receipt를 검증한 뒤에만 소유 token과 일치하는 sentinel을 지우고 쓰기를 재개한다. 쓰기 재개 전 실패는
+prior API/tunnel과 write availability를 복원한다. 쓰기 재개 뒤에는 old upload root로 자동 rollback하지 않는다.
+
## Task 설치 또는 승격
두 registrar 자체도 동일 stable release root에서 실행해야 한다. 다른 worktree의 registrar로 target만
@@ -61,12 +118,20 @@ apps/api/.env의 내용을 console이나 evidence에 출력하지 않는다. 새
$bootRegistrar = Join-Path $releaseRoot 'scripts\register-boot-task.ps1'
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $bootRegistrar `
- -StableSourceRoot $releaseRoot
+ -StableSourceRoot $releaseRoot `
+ -UserUploadDir $userUploadDir `
+ -UserUploadManifestPath $uploadManifestPath `
+ -ExpectedUserUploadManifestSha256 $uploadManifestSha `
+ -UserUploadWriteFreezePath $uploadFreezePath
if ($LASTEXITCODE -ne 0) { throw 'boot task 등록 실패' }
$watchdogInstaller = Join-Path $releaseRoot 'scripts\install-public-runtime-task.ps1'
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $watchdogInstaller `
-StableSourceRoot $releaseRoot `
+ -UserUploadDir $userUploadDir `
+ -UserUploadManifestPath $uploadManifestPath `
+ -ExpectedUserUploadManifestSha256 $uploadManifestSha `
+ -UserUploadWriteFreezePath $uploadFreezePath `
-IntervalMinutes 5
if ($LASTEXITCODE -ne 0) { throw 'watchdog task 등록 실패' }
@@ -88,14 +153,22 @@ RunNow 전에 action을 읽어 두 task가 같은 release root와 commit을 가
'-ExpectedSourceCommit',
'-ExpectedSourceTree',
'-ExpectedBootScriptSha256',
- '-ExpectedStartScriptSha256'
+ '-ExpectedStartScriptSha256',
+ '-UserUploadDir',
+ '-UserUploadManifestPath',
+ '-ExpectedUserUploadManifestSha256',
+ '-UserUploadWriteFreezePath'
)
VignettePublicRuntimeWatchdog = @(
'-StableSourceRoot',
'-ExpectedSourceCommit',
'-ExpectedSourceTree',
'-ExpectedWatchdogSha256',
- '-ExpectedStartScriptSha256'
+ '-ExpectedStartScriptSha256',
+ '-UserUploadDir',
+ '-UserUploadManifestPath',
+ '-ExpectedUserUploadManifestSha256',
+ '-UserUploadWriteFreezePath'
)
}
@@ -111,6 +184,9 @@ RunNow 전에 action을 읽어 두 task가 같은 release root와 commit을 가
if ($action.Arguments.IndexOf($commit, [StringComparison]::OrdinalIgnoreCase) -lt 0) {
throw "$taskName commit pin 누락"
}
+ if ($action.Arguments.IndexOf($userUploadDir, [StringComparison]::OrdinalIgnoreCase) -lt 0) {
+ throw "$taskName user upload root pin 누락"
+ }
foreach ($marker in $requirements[$taskName]) {
if ($action.Arguments.IndexOf($marker, [StringComparison]::Ordinal) -lt 0) {
throw "$taskName action pin 누락: $marker"
@@ -127,6 +203,21 @@ LastTaskResult=0과 stable release root의 public-runtime-watchdog.failcount=0
새 source 배포가 완료된 것은 아니다. 공개 API process cwd, Git commit, OpenAPI, auth, voice provider/model,
실제 session smoke까지 별도 배포 gate에서 확인한다.
+### PowerShell 5.1 web build 종료코드 경계
+
+Windows PowerShell 5.1의 `Start-Process -PassThru`가 반환한 `System.Diagnostics.Process`는 process handle을
+열기 전에 timed `WaitForExit(milliseconds)`를 호출하면 성공한 자식 프로세스도 `ExitCode=$null`로 남을 수 있다.
+`start-public-runtime.ps1`은 web build 직후 `$null = $build.Handle`로 handle을 먼저 확보하고, bounded wait 뒤
+`Refresh()`·null guard·nonzero guard 순서로 판정한다. null을 0으로 간주하거나 build를 무조건 재시도하지 않는다.
+
+계약 검증은 실제 Windows PowerShell 5.1에서 성공 프로세스의 종료코드를 읽는 probe를 포함한다.
+
+ py -3.11 -B -X utf8 -m pytest -p no:cacheprovider scripts\test_start_public_runtime_contract.py -q
+
+2026-08-29 기준 32 passed이며, detached-clean `44b7835c…`·tree `06133249…`에 boot/watchdog을 같은 핀으로
+재등록한 뒤 5174 자동복구·HTTP 200, `LastTaskResult=0`, failcount 0을 확인했다. 실제 Windows 재부팅 smoke는
+별도 운영 gate다.
+
## 숨김 수동 Trigger
watch-public-runtime-hidden.vbs는 source script를 직접 실행하지 않는다. 등록된 watchdog task action이
@@ -144,6 +235,39 @@ install-public-runtime-task.ps1은 액션을 `wscript.exe "\scripts\watch-
번쩍이고, 5분 주기 watchdog에서는 그것이 곧 "5분마다 화면에 뜨는 콘솔 창"이 된다(2026-08-08/09/12 세 번 재발).
런처는 pin 인자를 해석하지 않고 그대로 전달만 하며, provenance 검증은 watch-public-runtime.ps1이 수행한다.
+## Docker Desktop ERROR 1920 stale AF_UNIX socket 복구
+
+Docker Desktop 백엔드 로그 또는 `%LOCALAPPDATA%\Docker\backend.error.json`에 아래 경로의 `remove ...
+The file cannot be accessed by the system`(ERROR 1920)이 보이면 `com.docker.service`나 PostgreSQL volume 문제가
+아니다. 비정상 종료 뒤 남은 0바이트 AF_UNIX reparse socket 때문에 백엔드가 startup crash-loop한 것이다.
+
+- `%LOCALAPPDATA%\Docker\run\dockerInference`
+- `%LOCALAPPDATA%\docker-secrets-engine\engine.sock`
+
+Docker upstream의 [desktop-feedback #531](https://github.com/docker/desktop-feedback/issues/531)과
+[#536](https://github.com/docker/desktop-feedback/issues/536)에 같은 결함과 workaround가 기록돼 있다. 개별 socket은
+`Remove-Item`, `fsutil`, 파일 rename으로도 ERROR 1920이 날 수 있으므로 **삭제·factory reset·WSL unregister를 하지
+않는다.** Docker Desktop과 Docker CLI만 완전히 종료한 뒤 두 부모 디렉터리를 복구 가능한 timestamp 백업명으로
+옮긴다.
+
+ $stamp = Get-Date -Format 'yyyyMMdd-HHmmss'
+ $run = 'C:\Users\encep\AppData\Local\Docker\run'
+ $secrets = 'C:\Users\encep\AppData\Local\docker-secrets-engine'
+ if ((Resolve-Path -LiteralPath $run).Path -ne $run) { throw 'run 경로 불일치' }
+ if ((Resolve-Path -LiteralPath $secrets).Path -ne $secrets) { throw 'secrets 경로 불일치' }
+ $runBackup = Join-Path (Split-Path -Parent $run) ("run.stale-$stamp")
+ $secretsBackup = Join-Path (Split-Path -Parent $secrets) ("docker-secrets-engine.stale-$stamp")
+ if (Test-Path -LiteralPath $runBackup) { throw 'run 백업명 충돌' }
+ if (Test-Path -LiteralPath $secretsBackup) { throw 'secrets 백업명 충돌' }
+ Move-Item -LiteralPath $run -Destination $runBackup
+ Move-Item -LiteralPath $secrets -Destination $secretsBackup
+
+Docker Desktop 일반 사용자 재기동 뒤 `docker version`의 Linux server 응답을 확인한다. DB는
+`docker inspect vignette-dev-db`로 exact container와 recovered named volume이 존재함을 먼저 확인한 경우에만
+`docker start vignette-dev-db`를 실행한다. 새 container/volume 생성, 기존 volume 교체, `compose down -v`는 금지다.
+DB healthy와 55432 listener가 닫힌 뒤에만 아래 stable-root API-only 복구로 이어간다. WSL2 Linux engine에서
+`AlwaysRunService=false`이면 `com.docker.service`가 stopped인 사실만으로 장애 원인이나 복구 완료를 판정하지 않는다.
+
## Manual Source Recovery
운영 code를 강제로 교체해야 할 때도 shared worktree의 start-public-runtime.ps1을 실행하지 않는다.
@@ -152,6 +276,10 @@ install-public-runtime-task.ps1은 액션을 `wscript.exe "\scripts\watch-
$startScript = Join-Path $releaseRoot 'scripts\start-public-runtime.ps1'
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $startScript `
-Workspace $releaseRoot `
+ -UserUploadDir $userUploadDir `
+ -UserUploadManifestPath $uploadManifestPath `
+ -ExpectedUserUploadManifestSha256 $uploadManifestSha `
+ -UserUploadWriteFreezePath $uploadFreezePath `
-ForceApiRestart `
-SkipEngineRestart `
-SkipWebRestart `
@@ -178,6 +306,10 @@ commit/tree와 Python/cloudflared/config SHA를 read-only로 고정하고, confi
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $startScript `
-Workspace $releaseRoot `
+ -UserUploadDir $userUploadDir `
+ -UserUploadManifestPath $uploadManifestPath `
+ -ExpectedUserUploadManifestSha256 $uploadManifestSha `
+ -UserUploadWriteFreezePath $uploadFreezePath `
-ForceApiRestart `
-SkipEngineRestart `
-SkipWebRestart `
@@ -190,8 +322,10 @@ commit/tree와 Python/cloudflared/config SHA를 read-only로 고정하고, confi
-RuntimeProvenancePath $receipt
if ($LASTEXITCODE -ne 0) { throw 'fresh public provenance 승격 실패' }
-receipt에는 raw command line·config contents를 넣지 않고 PID/start/executable·command SHA/실제 cwd와 topology 입력만
-남긴다. 이 receipt의 PID와 pin을 `run-g7-external-proof-window.py --topology-mode windows-host`에 그대로 전달하고,
+receipt에는 raw command line·config contents를 넣지 않고 PID/start/executable·command SHA/실제 cwd,
+secret이 아닌 resolved `user_upload_root`, migration manifest SHA, 초기/current reference count와 privacy-safe digest,
+write-freeze path hash, topology 입력만 남긴다. 이 receipt의 PID와 pin을
+`run-g7-external-proof-window.py --topology-mode windows-host`에 그대로 전달하고,
공개 health·auth·OpenAPI·local provider ready를 확인하기 전에는 task action을 새 root로 재등록하지 않는다.
## Read-only CheckOnly
@@ -211,6 +345,7 @@ watchdog script를 직접 CheckOnly로 실행할 때도 task와 같은 pin을
-ExpectedSourceTree $tree `
-ExpectedWatchdogSha256 $watchSha `
-ExpectedStartScriptSha256 $startSha `
+ -UserUploadDir $userUploadDir `
-CheckOnly
추가 public host는 DNS와 routing이 실제로 열린 뒤 installer의