정식 승격 상태판과 증거를 동기화

This commit is contained in:
Yun Chan 2026-08-30 00:06:45 +09:00
parent 34aff65cb0
commit be08c0b573
16 changed files with 1160 additions and 63 deletions

View file

@ -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가 정상이다. 기존 인증 세션은 복사하지

View file

@ -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."
}
}

View file

@ -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": "<release-root>/apps/api",
"user_upload_dir_environment": "미설정",
"effective_user_upload_dir": "cwd 기준 상대경로 uploads",
"effective_storage_location": "<release-root>/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, 공개 재시작 증거를 포함해 마이그레이션한다."
}
}

View file

@ -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
}
}

View file

@ -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 계약 유지"
]
}
}

View file

@ -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
}

View file

@ -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"
}
}

View file

@ -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 <yunchan@twentyoz.kr>`, 한글의 짧은 커밋 메시지를 사용한다.
3. `git add .` 금지.
4. master `ac9b7026` 이후 runtime commit을 만들고, 새 clean release worktree/branch를 만든다.
5. UI 커밋 `35a62fda`, `a73b9eff`를 순서대로 cherry-pick한다.
6. clean candidate SHA/tree, clean status, 테스트 결과를 고정한다.
### 9.5 사용자 승인 후에만 정식 전환
후보가 GREEN일 때 사용자에게 아래 범위를 정확히 제시하고 승인받는다.
> 후보 커밋 `<sha>`를 원격에 push하고 Cloudflare Pages production과 이 PC의 public API/tunnel·두 예약 작업을 새 detached runtime으로 전환해도 돼? 기존 API/tunnel은 약 1분 재시작되고, 검증된 upload inventory는 덮어쓰기 없이 새 영구 root에 보존돼.
승인 전 금지:
- `git push`
- Cloudflare Pages production deploy
- public API/cloudflared stop/restart
- scheduled task reinstall/retarget/enable 변경
- DB avatar URL 수정
- stable upload root 생성/복사
승인 후에도 Pages 자산 보존은 stale default script를 그대로 쓰지 않는다. 최소 다음 실제 production 세대의 immutable asset graph를 explicit origins로 보존한다.
- `https://0c60261e.vignette-b1q.pages.dev`
- `https://ef48c0ae.vignette-b1q.pages.dev`
- `https://1f1ddf18.vignette-b1q.pages.dev`
배포 뒤에는 HTTP 200만 보지 않는다. custom domain의 신규 index/asset hash·MIME·신규 UI marker·구버전 marker 부재, API health manifest/DB/freeze proof, Google 로그인, super account role/onboarding, 데이터/아바타 fallback을 Codex 내장 브라우저로 보여준다.
### 9.6 실제 재부팅 게이트
production 전환과 browser smoke가 끝난 뒤에만 아래 문구로 명시 승인받는다.
> 지금 이 PC를 재부팅해도 돼. 저장하지 않은 작업은 없고, encep 계정으로 로그인한 뒤 REQ-008 자동복구 smoke까지 계속 진행해.
재부팅 뒤에는 먼저 task를 수동 실행하지 않는다. 로그인 후 자동으로 API/tunnel/tasks가 복구되는지 관찰하고, 공개 health·Google 로그인·데이터·이미지·새 runtime SHA/task action을 증명한다.
## 10. C001 외부 임상 검수 게이트
C001은 코드·UI·운영 배포로 대신할 수 없다. 적격 검수자의 실제 입력, 자격/역할, 검토 시각, 대상 버전/패키지 결속, 승인 또는 수정 요청을 받아 workbook의 지정 셀에 반영해야 한다. 검수자가 없으면 최종 Goal은 `기술 완료 / 외부 검수 대기`로 정확히 남긴다.
## 11. 절대 하지 말 것
- 손상 아바타 6개의 DB URL이나 파일을 승인 없이 삭제·초기화하지 않는다.
- 90개 decode-invalid legacy payload를 정상 이미지로 간주하지 않는다.
- 한 개 source root만 복사해 8개 DB ref를 복구했다고 주장하지 않는다.
- old runtime root 2개만 보고 데이터가 온전하다고 판단하지 않는다.
- general `/uploads` directory를 static mount하지 않는다.
- production Uvicorn worker를 2개 이상 띄우지 않는다.
- reset receipt 없이 initial nonzero refs → current zero를 정상으로 받아들이지 않는다.
- 예약 작업을 새 정의로 교체한 뒤 검증 전에 enable하지 않는다.
- 실제 재부팅 전 task를 수동 실행해 자동복구 증거를 오염시키지 않는다.
- 외부 임상 검수 내용을 만들어내지 않는다.
- dirty tree에서 전체 stage/commit/copy하지 않는다.
- Python 3.12/3.14 tempfile ACL 오류를 제품 테스트 실패와 혼동해 같은 방식으로 반복하지 않는다.
## 12. 재개 프롬프트
다음 세션에서 아래처럼 시작하면 된다.
> `docs/ops/handoff-goal-production-2026-08-29.md`를 먼저 읽고, live/Git/DB/avatar decode truth를 read-only로 재검증해. 손상 DB-ref 6개의 보존·fallback 정책과 `preserved_total_size_bytes=52973` end-to-end 결속부터 마무리하고, 전체 통합 GREEN 전에는 stage/push/deploy/runtime/task/DB를 건드리지 마. clean candidate가 준비되면 SHA와 승인 범위를 먼저 보여줘.

View file

@ -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 = '<approved lowercase SHA256>'
$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 "<root>\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 "<root>\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의