회기 무발화 0턴 분리, 자기예측 락 불변식 및 TDD 회귀 검증 완료
Some checks failed
API contract / OpenAPI type drift (push) Failing after 3m27s

This commit is contained in:
Yun Chan 2026-09-08 23:28:06 +09:00
parent a479db7a5a
commit a0311c5957
100 changed files with 4884 additions and 11210 deletions

View file

@ -0,0 +1,872 @@
# Vignette G0~G8 완료 추진 핸드오프
> Updated: 2026-08-18 KST (엔진 readiness 장애 수정·재승격 · 유스케이스 TDD 239 · 공개 Web 배포)
> Workspace: `D:\workspace\vignette`
> 이 문서는 다음 에이전트가 **G0~G8 아홉 목표 중 마지막 G7 외부 게이트를 닫기 위한** 실행 기준이다.
## 0. 현재 상태 한 줄 요약
**G0~G6의 내부 구현·증거와 G8 clean-head 배포·rollback 증거는 종료됐다. G8은 source HEAD
`61a41d1f…6af`·tree `87dec55d…3b77`·archive `4d15d055…119d4d`로 candidate 112/112와 actual NAS-origin
112/112를 모두 통과했다. 다만 2026-08-12 current NAS는 고정 LAN engine relay 상실로 DEGRADED이며 15:24 KST
health 요청도 3회 타임아웃이라, 과거 증거와 현재 runtime GREEN을 분리한다. 공개 앱은 health ok·db/engine true·
OpenAPI 126·auth 401·local voice exact를 유지한다. 앞선 fail-closed·verified rollback 이력과 previous
`6030a677…c611`은 보존했다. G7 공개 런타임은
detached-clean `a73bcd24…`·tree `b02b3a1b…`로 fresh 승격했고 OpenAPI 126, local Whisper/MeloTTS, Uvicorn queue 4,
API/cloudflared provenance receipt와 source-pinned Scheduled Task result 0을 확인했다. 인증 WSS·runtime·Windows topology의
30초 무마이크 rehearsal도 통과했다. 남은 것은 명시 동의 물리 마이크 3,120초·공통 3,000초 high-water,
독립 human pack과 canonical checker exit 0이다.**
| Goal | 판정 | 완료 증거 |
|---|---|---|
| G0 Measurement Foundation | DONE | provenance census 29/29, unknown/missing/orphan/null/total violations 0 |
| G1 Alliance Core | DONE | prompt 1.2 read-skew fix, prediction 24/24 ready, direction 9/9, error 0, recall 94.4%, precision 73.9% |
| G2 Outcome Trajectory | DONE | benchmark accuracy/early-warning recall 1.0, false alert 0, 실 DB 10 sessions/30 measurements |
| G3 Rupture & Repair | DONE | macro-F1 1.0, status 1.0, gaming 0, missed→partial→resolved 실 DB/API |
| G4 Deliberate Practice | DONE | completed-session observer, actual API/DB attempts, teacher correction, returned-practice browser 폐루프 |
| G5 Calibration & Transfer | DONE | actual transfer execution, independent/model-run/instrument/evidence provenance, DB/browser idempotency |
| G6 Supervision & Research | DONE | safety event metadata-only projection, safety priority 1 > deterioration 2, raw detail 0 |
| G7 Multimodal Alliance | **internal DONE · external GATE** | consent/withdrawal, synthetic soak 59/59, public `a73bcd24…`·OpenAPI 126·local voice, no-mic rehearsal 3 legs × 7 samples |
| G8 Continuous Improvement | **증거 DONE · CURRENT NAS DEGRADED** | 실제 image rollback 2회 + executed receipt 2건, source/archive 결속, candidate/origin 112/112; current engine relay 복구·재검증 대기 |
대시보드 status count는 **33 done / 2 doing / 0 planned**다.
G8 배포 source 기준선은 branch `master`, HEAD `61a41d1f08239b5f9e99cfae42a206be250416af`·tree
`87dec55daf0a22ca5c2e489cd803a2f731e63b77`이며 2회 동일 archive SHA는
`4d15d055d74f33f1fa2ff470afbf900d565ab238b19c79df34ca7ba240119d4d`다. 공유 worktree의 후속 변경은 이
배포 source에 포함되지 않으므로 G8 완료 증거와 섞지 않는다.
### 2026-08-18 세션 — 엔진 장애 수정·유스케이스 TDD·재승격
- **공개 엔진 3.5시간 장애(06:30~10:05 KST)**: `reasoning_effort=high` 콜드 프로브가 20초를 넘기면
실패가 성공과 같은 TTL(운영 1800초)로 캐시돼 공개 API가 `engine=false`로 오염됐고, 사용량 한도
소진 구간에는 `Reached maximum budget ($0.5)`로 이어졌다. 워치독은 기본 `/ready`의 캐시 200만 보고
API만 재시작하는 루프에 빠졌다. 수정: `ENGINE_READY_FAILURE_TTL_SECONDS`(기본 30초) 분리 +
`ENGINE_READY_TIMEOUT_SECONDS` 20→45초, 회귀 4건(`engine_gateway/test_ready_cache.py`).
gateway 62/62 · backend 986 passed · ruff clean.
- **운영 경계 기록**: 공개 엔진 `claude -p`는 소유자 Claude 구독을 개발 에이전트 작업과 공유한다.
대량 병렬 에이전트 실행은 공개 서비스의 생성 경로를 굶길 수 있다(이번 장애의 2차 원인).
- **공개 런타임 재승격**: detached-clean `4771b97c…c880`·tree `286723de…c1fa` 루트로 API/게이트웨이/
cloudflared를 교체하고 워치독·부트 task를 같은 루트에 SHA pin 재등록, 명시 실행 `LastTaskResult=0`.
1차 승격은 새 worktree에 gitignored `apps/api/.env`가 없어 fail-closed로 실패했다(약 4분 다운타임).
**다음 승격부터는 시작 전에 이전 루트의 `apps/api/.env`를 새 루트로 복사하라.**
- **유스케이스 TDD 캠페인**: 16테마 `uc-*.spec.ts` 239 시나리오(수집 1109 tests/61 files)를 RED 우선으로
작성했다. 제품 결함 수정 4묶음(설정 지원 티켓 작성 UI 부재, 관리자 해결 노트 입력 부재,
포커스 링 가림, 라벨/헤딩/대비/44px/오버플로 결함)과 기존 스펙 드리프트 교정을 닫았다.
증거: `docs/ops/evidence/usecase-tdd-2026-08-18.json`.
- **직렬 게이트(e2e:single-run) 후속**: 11:10 KST 실행은 52 passed/5 failed/4 skipped. 삼각화 결과
breakpoint-sweep 실패는 `widthsFor` 솎아내기가 실기기 대표 폭을 탈락시키는 테스트 결함(교정 후 passed),
kb-source-packs 500은 수정 커밋 이전에 기동한 낡은 dev API(:8000) 프로세스가 원인(재기동 후 passed),
voice-success·session-persistence 실패는 Claude 구독 세션 한도(resets 3pm)로 엔진 502가 난 환경 결함.
교사 재평가 2건은 엔진 가용 게이트가 skip 처리하며 **15:00 KST 리셋 후 재검증이 열려 있다**.
- **공개 Web Cloudflare Pages 배포**: 소유자 지시로 detached-clean `cf899b7f…7c96` 후보
(`D:\workspace\vignette-public-web-cf899b7f`)를 빌드×2 결정성(599파일 sha256 전일치) 확인 뒤
production `4edc02d3`로 배포했다. 7개 origin(커스텀 도메인+직전 6세대)에서 143자산을 보존해
최종 742파일, 커스텀 도메인은 새 엔트리 `index-BWxBaCPM.js`/`index-BUfQc2Rm.css`를 서빙하고
직전 엔트리 자산도 200으로 살아 있다. 증거: `docs/ops/evidence/public-web-deploy-2026-08-18.json`.
- **NAS 엔진 릴레이 복구(1단계 종료)**: `start-nas-preview-engine.ps1 -CheckOnly` 프리플라이트가
4종 SHA pin + LAN `192.168.0.223:9100` 바인딩으로 통과했다(mutation=false,
영수증 `D:\workspace\nas-relay-state\receipt-checkonly-20260818.json`). env는 NAS preview API 실행
컨테이너의 `ENGINE_GATEWAY_SHARED_SECRET`을 값 노출 없이 로컬로 반출해 결속했다. 실제 1회 시작은
엔진 한도 리셋(15:00 KST) 뒤 15:05 자동화가 같은 핀으로 수행하고 NAS health 3회·auth 401·OpenAPI 126을
재검증한다. 증거: `docs/ops/evidence/nas-relay-recovery-2026-08-18.json`.
- **agy 공급자 복구(2026-08-18 오후)**: 관리자 AI 운영에서 agy 전환이 막힌 이중 결함을 고쳤다 —
`agy models``id<TAB>라벨` 형식으로 바뀐 것을 파서가 못 읽음(5afd92f8), ② 승격 체인의 psutil
환경 이식에서 SystemRoot 유실로 Go 계열 CLI가 빈 목록을 반환(6369f294 — `_cli_subprocess_env`
백필 + 런처 Machine 스코프 병합). 공개 런타임을 `6369f294`로 승격하고 워치독·부트 태스크를 모두
새 루트에 재핀했다(**워치독은 `install-public-runtime-task.ps1`, 부트는 `register-boot-task.ps1`
승격 시 둘 다 실행해야 구 루트 되살림 경합이 없다**). 검증: capabilities 14모델 + 실생 턴
`gemini-3.6-flash-high` 응답·비용 계약 통과. 증거: `docs/ops/evidence/agy-provider-recovery-2026-08-18.json`.
- **회기 종료 503 핫픽스(bf5f7352)**: `end_session`의 pinned_fact upsert가 learner 역할 연결로 실행돼
RLS에 거부되던 잠복 결함 — pinned_fact의 SELECT/UPDATE 정책은 AI 뷰·admin 전용이다. 같은 learner
uid로 `acquire(ai_view="evaluator")`를 열어 저장을 분리했고, 공개 DB 롤백 재현(learner 거부·AI 뷰
통과)으로 실증했다. 엔진 건강 시 deep 평가가 팩트를 반환하는 순간부터 발화하므로, degraded 구간
E2E가 이 경로를 덮지 못한 이유도 같다. 소유자 실세션 종료 성공과 로컬 실API E2E 2회(종료 200 +
pinned_fact 실적재 실증)로 운영 검증했다.
- **2026-08-18 심야 연쇄 복구·NAS current GREEN**: 대청소 사건(node_modules·구 런타임 워크트리·Temp
소실)으로 릴레이가 죽고, Docker Desktop 사망·whisper 9882 WinNAT 포트 예약·DHCP의 LAN IP 회수
(223→224)가 겹쳤다. 릴레이를 현행 루트에 같은 핀으로 재기동하고 224로 계약 이전(방화벽 재발행·NAS
`.env` 재구성), 클로드 예산 한도 재발으로 NAS를 `agy_cli`로 전환해 health 3/3 engine true·auth 401·
OpenAPI 126·actual-origin turn→review smoke를 모두 통과 — 201차 current GREEN 달성. 권고 3건은
`docs/ops/evidence/nas-relay-recovery-2026-08-18.json` 참조.
### 이 문서가 인계된 정확한 중단점
- G8 clean-head execute는 격리 NAS preview만 변경했고 공개 API/DB·물리 마이크는 건드리지 않았다. source HEAD/tree와
archive 2회 결정성, fresh dump, exact images, candidate/actual-origin 결과를 모두 결속했다.
- G8 종료 뒤 공개 Web은 Pages production deployment `7bd59055…`로 갱신했다. 실제 사용자 회기에서 literal
`[NAME]` 0, `시간 만료`, 모바일 44px·자동 스크롤 보정을 브라우저로 확인했다. 공개 API는 아래 G7 clean
promotion에서 별도로 fresh 승격했다.
- active goal은 계속 `active`다. 목표는 “문서 작성”이 아니라 **G0~G8 아홉 목표를 실제 runtime과 E2E로
모두 닫는 것**이다. G8은 종료됐고 G7 canonical checker exit 0이 없으면 `goal complete`를 호출하지 않는다.
- current NAS 복구용 `scripts/start-nas-preview-engine.ps1`은 source-pinned fail-closed 계약으로 보강했다.
detached-clean HEAD/tree, launcher·Python·env SHA, exact consumer URL, explicit LAN 승인, 외부 state/receipt,
listener owner와 generation readiness를 강제하고 loopback contract **8/8**을 통과했다. 2026-08-12에는
detached-clean `f08e03de…f191`·tree `00275133…0abd`에서 exact `-CheckOnly`도 PASS했다. launcher/Python/env
SHA는 `492d8b5c…b27c`/`5f7b89a6…52ec`/`e53acedf…8a39`, `mutation=false`, listener `0→0`, runtime
state 생성 0이다. 실제 9100 listener, NAS 설정·컨테이너·DB는 건드리지 않았다. 다음 mutation은 소유자 승인 뒤
단일 owner가 actual 1회→NAS health/auth/OpenAPI/assets/실회기 재검증 순서로만 수행한다.
- 최신 full disposable GREEN은 공통 톱바 44px 계약을 포함한 clean `b02bee26…`·tree `8f677bba…`
`periodic-learner-e2e-20260812-171547.json`·SHA-256 `8c11a136…af8e3`이다. 487.3초에 동일 학습자
브라우저 회기 생성·SSE·review ready·G4/G5 `0→1→1`, 별도 returned desktop/mobile 4/4와 route mock 0을
통과했고 public/NAS/active DB 접촉 0, container·volume·network·listener·temp 잔여 0을 독립 재확인했다.
- 공개 Web 보정은 detached-clean `00f851a6…c229`·tree `9f78cd16…9486` 후보로 배포 직전까지 고정했다.
API generated type LF 계약, `npm ci --ignore-scripts`, API type contract, typecheck, 2회 byte-identical production
build와 상단 44px·로그인 무가로넘침·자기주도 폐루프 focused E2E 12/12를 통과했다. 새 primary CSS/JS는
`index-Dzmt2kJS.css` SHA `da5e8535…fb9a``index-BUoTLplD.js` SHA `08654d08…3c05`다. 현재 공개와 최근
Pages 5개 origin의 지연 로드 자산 133개를 더해 최종 732파일로 만들었고 현행 공개 CSS/JS SHA도 exact
보존했다. actual 공개는 health ok·db/engine true지만 구 Web이라 모바일 상단 6개 조작면이 44px 미만이고,
NAS는 health timeout·9100 listener 0·로그인 overflow 10px다. 공개/NAS 배포는 0이며 기계 판독 증거는
`docs/ops/evidence/public-web-preflight-2026-08-12.json`이다.
- 앞선 두 DB init 실패는 `core.autocrlf=true` detached checkout이 `99_app_role.sh`를 CRLF로 만든 것이었고,
`.gitattributes``*.sh text eol=lf` 뒤 새 worktree에서 tracked shell CRLF 0·해당 파일 CR 0과 실제 DB healthy를
확인해 닫았다. 공통 톱바의 브랜드·공간 전환·테마·로그아웃은 desktop/mobile 모두 44px이고, auth overflow와
함께 source/full E2E GREEN이지만 아직 공개/NAS에는 배포하지 않았다.
- 마지막 완료 작업은 G8 source HEAD `61a41d1f…6af`·tree `87dec55d…3b77`·archive `4d15d055…119d4d`
candidate 112/112와 actual NAS-origin 112/112다. API/Web `d5021950…e4b1`/`9796c092…4b36`, health
ok·db/engine true·auth 401·OpenAPI 126, fresh dump `36ec8748…24db8`을 확인했다.
- G7 Windows topology mode, 3,000초 공통 시간창, canonical checker-bound exit와 공개 런처의 local
Whisper/MeloTTS lifecycle·exact readiness·Uvicorn queue·fresh API/cloudflared provenance 계약은 코드와 공개 runtime에서
확인했다. detached-clean `a73bcd24…`·tree `b02b3a1b…`의 receipt `47388d58…9b08`가 passed이고,
OpenAPI 126·`/admin/voice-runtime`·`local_whisper/small`·`melotts/melotts-korean`·queue 4가 공개 경로와 일치한다.
- **public task source-pin P0 완료:** watchdog·로그온 boot는 detached clean release의 commit/tree와
watchdog|boot/start script SHA를 mutation 전에 검증한다. 두 Scheduled Task를 같은 `a73bcd24…` stable root에
재등록하고 명시 실행해 `LastTaskResult=0`·Ready와 working directory pin을 확인했다. 실제 Windows 재부팅 뒤
자동복구 smoke는 별도 운영 게이트로 남는다.
- 인증 WSS ready/ping/close 1000, runtime 7 samples, Windows topology 7 samples의 30초 무마이크 rehearsal을 통과했다.
voice/runtime/topology evidence에는 UUID/email literal이 없고 `physical_capture=false`다. 다음 mutation은 사용자가
정확한 장치를 선택하고 명시 동의한 뒤의 3,120초 마이크 캡처다. G8 NAS는 새 material milestone이 생기기 전까지
읽기 전용 기준선으로 둔다.
### 완료 조건 판정 (2026-08-09 재검증)
원래 active goal의 완료 선언 조건 네 가지를 실행으로 대조한 결과다.
| # | 조건 | 판정 | 근거 |
|---|---|---|---|
| 1 | G0~G8 각각의 코드 계약과 실제 runtime 증거 | **부분 충족** | G0~G6과 G8 종료 증거는 충족. current NAS runtime 복구와 G7 외부 GATE가 열려 있다 |
| 2 | 학생 홈→회기→리뷰→처방→재연습→전이 폐루프가 실제 API/DB/browser 통과 | **충족** | 일회용 클론에서 `returned-practice-db-closed-loop` desktop 2/2 + mobile 2/2, route mock 0 |
| 3 | current source 증거와 대시보드·TODO·backlog 일치 | **충족** | SSOT checker FAIL 0, dashboard E2E 10/10, count 33/2/0이 카드와 일치 |
| 4 | G7을 가짜·합성 증거로 DONE 처리하지 않음 | **충족** | `check-g7-external-proof.py` exit 1 유지, G7 카드는 `GATE` |
재검증 수치: API **921 passed**, gateway **58**, executor **28**, probe **11**, helper launcher **37**,
release agent **30**, current G7 종료 도구 **104/104**(runtime/checker 6파일 94 + intake 6 + standalone validator 4), API voice 통합 **71**,
MeloTTS 사이드카 **16**, local whisper 사이드카 **38**,
SSOT FAIL **0**,
dashboard E2E **10/10**, web api-types·typecheck·build 통과, ruff clean.
current-source fixture 묶음은 16:49 KST 단일 실행에서 **120/120 PASS(92.0s)**했다. dev dashboard file URL은 실행 cwd가
아니라 spec의 `import.meta.url`로 repo root를 찾으며, session-review fixture는 alliance pulse까지 소유한다. 관리자
live health 테스트는 React StrictMode 중복 조회의 첫 응답을 고정하지 않고 화면이 실제 반영한 성공
`/admin/users` 스냅샷과 KPI를 결속한다. 그 뒤 dev-dashboard spec/HTML이 변경됐고 최신 dashboard 10/10만 따로
재검증됐다. 따라서 current tree의 정직한 판정은 **110 불변 + 10 최신 분할 GREEN**이며, 새 clean commit에서
같은 120건을 한 번 재실행해 archive/source 결속 뒤의 결과를 고정한다.
**따라서 `update_goal complete`를 호출하지 않는다.** 조건 1이 G7 external proof 때문에 미충족이고,
조건 4가 G7을 합성 증거로 메우는 것을 금지한다. G8은 clean-head NAS-origin까지 종료됐다(§3·§5).
### G0~G8 목표별 인수 기준
| Goal | 원 계획의 종료 조건 | 현재 인정 가능한 증거 | 다음 에이전트가 최종 종료 전에 할 일 |
|---|---|---|---|
| G0 | 100% provenance, evaluator/client 누수 0, Python/TS/DB schema conformance, 기존 회기 무회귀 | current producer census 29/29, 위반 0, migrations 14~16 2회 멱등 | current HEAD 전체 backend/schema 회귀와 최종 census를 다시 실행 |
| G1 | goal/task/bond 독립 저장, 축별 UI, gold 근거 span, 평가 실패 fail-closed | prediction 24/24 ready, 방향 9/9, error 0, recall 94.4%, precision 73.9%, pre/mid/post UI | current HEAD의 alliance desktop/mobile·DB-backed session 경로 재실행 |
| G2 | 5회기 연속성, 조기 경보 recall, false alert/uncertainty, synthetic 비임상 경계 | accuracy/recall 1.0, false alert 0, 실제 DB 10 sessions/30 measurements | current HEAD outcome trajectory API/UI 회귀 재실행 |
| G3 | rupture 유형 benchmark, missed/partial/resolved E2E, gaming/암기 방지 | macro-F1/status 1.0, gaming 0, 실제 DB/API missed→partial→resolved | text/SSE 회기 및 rupture UI desktop/mobile 재실행 |
| G4 | 모든 코칭 카드의 실행 재연습, 전후 근거 저장, unseen 전이 전 mastery 금지 | 실제 API/DB attempt·observer·teacher correction, browser route mock 0 | disposable DB에서 returned-practice desktop/mobile 4/4와 멱등 재확인 |
| G5 | 자기평가 잠금, calibration 감소, unseen transfer 유지, subgroup drift | actual transfer execution과 independent/model-run/instrument/evidence provenance | G4와 같은 disposable DB에서 actual POST·read reload·1→1 멱등 재확인 |
| G6 | 3-click queue, 원장 drilldown, 버전 재현, 역할/RLS/audit | safety metadata-only, safety priority 1 > deterioration 2, raw detail 0 | current HEAD producer/store/API와 supervisor UI 회귀 재실행 |
| G7 | 시간 정렬, **물리 마이크+공개 WSS 장시간 E2E**, text-only 대비 음성 이득, 동의/삭제 정책 | 내부 계약·공개 clean runtime·인증 WSS 무마이크 rehearsal 완료. canonical checker는 의도적으로 미실행/미종료 | 장치 선택·명시 동의→mic 3,120초→동시 runtime/topology→독립 human pack→checker exit 0 |
| G8 | source→draft→red-team→benchmark→catalog 재현, 누수/PII/무근거 0, 자동 calibration+rollback, incident→test/backlog 환류 | agentic worker·human gate·실제 rollback/restore receipt 2건, clean-head candidate/NAS-origin 112/112 | **종료.** 새 material milestone 전까지 NAS 기준선 보존 |
## 1. 작업 시작 전 반드시 읽을 파일
1. `AGENTS.md`
2. `docs/README.md`
3. `docs/HANDOFF.md` — 현재 문서
4. `docs/dev_dashboard.html` — 상태 SSOT
5. `docs/TODO.md` — 열린 작업 SSOT
6. `docs/ops/backlog-2026-06-26.md`
7. `docs/ops/outcome-os-g7-external-proof-readiness-2026-08-07.md` — **다음 작업의 핵심**
8. `docs/ops/nas-preview-g8-rollback-proof-runbook.md`
9. `docs/ops/nas-preview-deployment-evidence-2026-08-07.md`
10. `docs/ops/public-db-recovery-rehearsal-2026-08-07.md`
`docs/archive/`는 현재 상태 근거로 읽지 않는다.
## 2. 절대 보존 경계
- Windows 11 + PowerShell 환경이다. 실행 전 OS·셸·도구 경로를 다시 확인한다.
- 명령은 PowerShell 5.1 호환 문법을 기본으로 작성한다. NAS 원격 스크립트는 **UTF-8 base64 전달 방식**을 쓴다.
(`$()`가 PowerShell에서 먼저 확장되는 것을 막는다.)
- G8 배포 source는 `master` / `61a41d1f08239b5f9e99cfae42a206be250416af`이며 공유 worktree의 후속 변경은
이 clean release와 분리한다. stage/commit 시 `git add -A`를 쓰지 않고 명시적 경로만 다룬다.
- clean-head 구현은 unit 30/30, G7 runner/checker/topology는 86/86, G7 public launcher/sidecar는 80/80,
public task source pin은 provenance 11개를 포함한 combined 22/22, timestamp·voice 계약을 포함한 API는
921/921와 KST 실DB browser 1/1, G8 candidate/NAS-origin은 각각 112/112,
SSOT는 FAIL 0와 dashboard E2E 10/10을 통과했다. 다음 에이전트는 최초 diff review 뒤 동일한 focused gate를
다시 실행하고, 서로 다른 lane의 파일을
누락한 채 clean commit을 만들지 않는다.
- 기존·watchdog 생성 엔진 로그 4개와 `apps/web/test-results/`의 PNG 2개는 commit에서 제외하고 보존한다. 신규
`apps/api/app/test_session_read_model.py`
`docs/ops/evidence/g8-clean-head-material-milestone-2026-08-09.json`은 이번 목표 산출물이므로 clean source에
포함해야 한다.
시작 즉시 `git status --short`로 다시 확인하고 `git reset --hard`, `git checkout --`, 대규모 자동 포맷, 임의 삭제는 금지한다.
- Git commit/stage/push는 사용자가 요청하기 전에는 하지 않는다.
- 공개 복구 DB, 원본 volume, pre-recovery container, dump, NAS named volume, 이전 API/Web 이미지를 삭제하지 않는다.
- **로컬 포트 8001(공개 API origin) · 55432(공개 DB) · 9099/9100(엔진)은 공개 런타임이다.**
공개 Web은 Cloudflare Pages이고 5174는 `vnet.18ka.net`용 로컬 preview다. `scripts/dev-up.ps1`은 런타임
리스너를 정리할 수 있으므로 공개 런타임이 떠 있는 동안 실행하지 않는다.
- 비밀값, 이메일, cookie, 사용자 UUID가 포함된 raw response, `.env` 내용은 로그·증거·채팅에 출력하지 않는다.
## 3. G8 receipt + clean-head NAS-origin — 종료됨
### Gate6 계약 정정 (성립 불가능했던 이전 계약을 코드로 교체)
감사 대상 current API 이미지 `sha256:52e0e816…8b2d``com.docker.compose.project=vignette-preview-20260807`,
`service=api`, `version=2.20.1` **image label**을 갖고 있다. 따라서 "helper의 `com.docker.compose.*` key 0개"는
감사되지 않은 다른 이미지를 쓰지 않는 한 성립하지 않는다. 계약을 **key 부재 → 소속(membership)** 으로 바꿔
구현했다.
- 코드: `scripts/launch-nas-preview-g8-helpers.py`
- 테스트: `scripts/test_launch_nas_preview_g8_helpers.py` — **37/37**
- 계약: image 상속 label을 baseline으로 읽고 container의 모든 compose label이 baseline과 같거나 선언된 격리
override인지 검사 · 최종 project ≠ target · 최종 service ∉ {api,web,db,proxy} · argv에 target project/service
label 미주입(fake-runner) · exact container name/ID/`docker inspect` 증거 보존
- 런북: `docs/ops/nas-preview-g8-rollback-proof-runbook.md`
### 실제 실행 결과
| plan | receipt | 활성화된 API/Web 이미지 |
|---|---|---|
| `rollback-old` | `nas-g8-723eeef22eab05e63e3fafb0` | `79ec…4450` / `c530…2f28` |
| `restore-current` | `nas-g8-2738846cf2cf4fbe8ce0fc26` | `52e0…8b2d` / `6fdb…f215` |
- release gate·approval 각각 2회 멱등, lifecycle `executed`, artifact/approval/receipt binding, control-plane 분리 검증
- DB: `audit.ci_lifecycle_event` `rollback/executed` 2 · `audit.ci_human_approval_event` `authorize_rollback` 2 ·
`app.ci_release_gate` 2(전부 `pending_human_approval`, silent auto-promotion 0) · `ci_gate_artifact` 8
- HMAC journal 6 records, `previous_hash` 체인 전수 PASS, SHA256 `a5594feb…0690`
- 최종 상태: health 3/3, OpenAPI 126, auth 401, Web 200, helper 0, listener 0, 비밀 env 파기
- **계획 이탈:** Windows SSH 터널 `18018→8018`은 NAS sshd가 `administratively prohibited`로 direct-tcpip를 거부해
쓸 수 없었다. **sshd 설정은 바꾸지 않았고**, 같은 격리 계약의 NAS-side probe 컨테이너에서 loopback으로
실행해 control-plane(8018)/preview(8088) origin 분리는 그대로 유지했다. 다음 에이전트도 NAS에서
포트 포워딩을 기대하지 말 것.
- 기계 판독 증거: `docs/ops/evidence/nas-preview-g8-actual-rollback-2026-08-07.json`
## 4. 공개 서비스 복구 상태 — 이미 완료, 다시 망가뜨리지 말 것
공개 계정·회기 DB 복구는 2026-08-07 18:26 KST에 완료됐다. 2026-08-09 14:03 KST에는 Docker Desktop이
중단돼 DB listener가 사라지면서 public health가 일시적으로 `db=false`가 됐고, 18:02:55 KST에는 Docker Desktop
UI가 active DB container에 직접 stop을 보내 같은 증상이 재발했다. 두 번 모두 보존된 동일 컨테이너·볼륨만
재기동해 복구했다. 새 컨테이너·볼륨 생성이나 데이터 복원 덮어쓰기는 없었다.
- Public Web `https://vignette.chanpaca.net` · Public API `https://api-vignette.chanpaca.net` (origin `127.0.0.1:8001`)
- active DB container `vignette-dev-db`, volume `vignette_recovered_prod_20260807`, restart `unless-stopped`
- cutover 기준 복구 집계: users 84, sessions 30, turns 705, Google users 16, orphan sessions 0
- 2026-08-09 18:05 KST 재기동 후 owner read-only 집계: users 356, sessions 193, turns 726, Google users 16,
Google-owned sessions 31. 조회는 `BEGIN READ ONLY``ROLLBACK`했고 PII를 출력하지 않았다.
- health `environment=prod`, `db=true`, `engine=true`, `engine_mode=claude_cli`, dev-login disabled, unauth `/auth/me` 401
- **2026-08-09 G7 promotion snapshot:** local/public health는 `status=ok, db=true, engine=true`, Google OAuth
start는 302→`accounts.google.com`, 공개 OpenAPI는 126 paths이고 `/admin/voice-runtime`이 존재한다.
`/voice/health``local_whisper/small`·`melotts/melotts-korean`·WS queue 4를 보고한다.
- DB 컨테이너는 `vignette_recovered_prod_20260807` named volume과 `unless-stopped`를 유지한다. API/cloudflared는
detached-clean `a73bcd24…`·tree `b02b3a1b…`에서 실행되고 receipt `47388d58…9b08`가 passed다.
watchdog·로그온 task도 같은 stable root/commit/tree/script SHA에 pin해 명시 실행 결과 0·Ready를 확인했다.
실제 Windows 재부팅 뒤 자동복구 smoke만 남아 있다.
- **2026-08-12 public reconnect incident:** 휴면 Gradle daemon이 `127.0.0.1:8001`을 점유했고 cloudflared와
9882/9883 sidecar가 없는 상태에서 PowerShell 5.1 native stderr가 start/watch/boot의 복구 분기를 조기 종료했다.
exact Gradle PID만 중지하고 DB dump `9cbda31e…0ce5`(7,532,625 bytes·TOC 1,492)를 먼저 고정했다. 복구 코드는
103/103과 PS5.1 parser를 통과했고, detached-clean `a73bcd24…`·tree `b02b3a1b…`에서 API/cloudflared를
fresh 교체했다. receipt `47388d58…9b08` passed, public health 3/3·db/engine true·OpenAPI 126·local voice exact,
웹 진단 overlay/console error/`[NAME]` 0, watchdog·로그온 boot 명시 실행 result 0을 확인했다.
보안상 과거 `auth_session`은 복원하지 않았다. 사용자는 Google 재로그인이 필요하다.
보존물: pre-recovery container `vignette-dev-db-pre-recovery-20260807-182642`과 원본/recovery Docker volumes,
그리고 2026-08-09 fresh custom dump
`D:\workspace\vignette-backups\vignette-dev-db-vignette-20260809-122235Z.dump`
(SHA256 `f1fd569c…5f64`, 7,458,327 bytes)다.
과거 증거에는 pre-cutover dump `…20260807-091057Z.dump` SHA256 `6b84d5c8…af1f`, recovered dump
`…20260807-092630Z.dump` SHA256 `d7bcc396…68ed`, post-cutover dump `…20260807-095511Z.dump` SHA256
`660695c8…65b88`이 기록돼 있다. 이 세 과거 dump의 현재 위치는 별도로 재확인하지 않았으므로 삭제로 단정하지 말고
off-host/NAS/다른 경로를 확인하기 전 restore·cleanup을 금지한다. NAS의 pre-proof/pre-clean-head dump는 §5 기준으로
실제 존재와 hash를 재확인했다.
공개 DB에는 current dirty source를 배포하지 않는다.
## 5. NAS 배포 기준선 — 2026-08-09 clean-head DONE
- URL `http://100.116.83.60:8088` · project `vignette-preview-20260807` · remote root
`/volume1/docker/vignette-preview-20260807`.
- source HEAD `61a41d1f08239b5f9e99cfae42a206be250416af` · tree
`87dec55daf0a22ca5c2e489cd803a2f731e63b77` · 2회 동일 active archive
`4d15d055d74f33f1fa2ff470afbf900d565ab238b19c79df34ca7ba240119d4d`.
- API `sha256:d502195021beafe869c555f3ec80269426062ef4586dc15a3e049f9c8660e4b1` / Web
`sha256:9796c0925f6a984cb846b61a4f13fda4d403bda7b3a2b6232b728cebbcdc4b36` exact image가 실행 중이다.
- candidate session E2E **112/112**, 실제 NAS 평문 origin browser review **112/112**(11 specs,
desktop/mobile/single-run), health `status=ok·db=true·engine=true`, auth 401, OpenAPI 126.
- fresh custom dump `vignette-preview-pre-clean-head-retry-61a41d1f-20260809T112038Z.dump`, SHA256
`36ec8748cf28c520d9489d108dee9d4a154e6260f6996b4da3e0fe3b8a624db8`, 1,097,100 bytes,
TOC 1,738 / TABLE DATA 129를 보존했다.
- previous active `6030a677af7e87cbfabc422b553d108d53414fd3c446548734a13b036d35c611`과 이전 images,
named DB volume을 삭제하지 않았다.
배포 중 첫 108 gate가 103/5로 fail-closed된 이력(G5 fixture drift, StrictMode retry fixture, `shell.css` manifest 누락)은
지우지 않는다.
### 과거 비-secure origin 결함 — 수정·배포 검증 완료
배포 후 NAS 프리뷰(`http://100.116.83.60:8088`, 평문 HTTP·비-localhost)에 전체 회기 스펙을 돌려보니 24건이
실패했고, 원인은 단 하나였다. 페이지 스냅샷의 실제 예외는 `crypto.randomUUID is not a function`이다.
이 API는 **secure context(HTTPS 또는 localhost)에서만** 노출되는데 제품 코드 18곳이 fallback 없이 직접
호출했고, `RuptureRepairCard.tsx`는 렌더 시점(`useRef`)에 호출해 회기 리뷰 라우트 전체가 error boundary로
떨어졌다. 릴리스 게이트의 108/108은 **localhost 후보 스택**(secure context)에서 돌았기 때문에 이 경로를
한 번도 밟지 않았다.
- 수정: `apps/web/src/lib/uuid.ts``randomUuid()`로 통일. fallback도 `crypto.getRandomValues`를 우선
사용해 idempotency key의 예측 불가능성을 유지하고, Web Crypto가 아예 없을 때만 `Math.random`으로 내려간다.
- 회귀: `apps/web/e2e/insecure-context-uuid.spec.ts` **6/6** (직접 호출 0건 검사 포함), typecheck·build 통과.
- UUID와 portable SHA-256 수정은 archive `4d15d055…119d4d`로 NAS에 배포했고 actual-origin 112/112를 통과했다.
- 교훈: candidate gate를 localhost에서만 돌리면 secure-context 전용 API 결함을 못 잡는다. 배포 대상과
같은 scheme/host 형태에서 최소 한 번은 회기 리뷰 라우트를 열어봐야 한다.
## 6. 다음 실행 순서 — G8 종료, G7 외부 종료
### 6.1 G8: clean-head 릴리스 경로 — DONE
`scripts/run-outcome-os-release-agent.py`의 명시적 clean-head 경로는 구현됐다. 기존 patch mode를 기본값으로
보존하며, tracked-clean source worktree의 exact HEAD/tree/archive만 candidate로 사용한다. 수동 복사나 NAS 직접
빌드는 계속 금지한다.
구현된 **`--source-mode clean-head`** 계약은 다음과 같다.
1. tracked dirty가 있으면 fail-closed. untracked 로그·테스트 결과는 archive에서 제외한다.
2. exact Git HEAD, tree, `git archive` SHA를 증거에 기록한다.
3. 같은 HEAD의 archive를 두 번 만들어 SHA가 동일한지 검증한다.
4. candidate는 그 archive만 풀어 만들고 patch/manifest를 암묵 적용하지 않는다.
5. 기존 preflight·API full·Web type/build·release E2E·DB snapshot·rollback 절차는 그대로 유지한다.
6. Vite source 전용 `e2e/insecure-context-uuid.spec.ts`는 별도 localhost gate(5198)로 12/12 실행한다. UUID fallback
6건과 SHA-256 fallback·직접 호출 금지 6건을 함께 고정하고,
production candidate/NAS-origin은 실제 배포 번들에서 동작하는 11개 회기 스펙만 실행한다.
7. unit/fake runner가 tracked dirty, archive SHA drift, evidence binding, patch-mode 무회귀를 검증해야 한다.
검증: release-agent unit 30/30, source-only insecure-context gate 12/12, Ruff, py_compile, `git diff --check` PASS.
최종 실행은 detached clean HEAD `61a41d1f…6af`에서 candidate 112/112와 actual NAS-origin 112/112를 통과했다.
이하 preflight와 실패 기록은 최종 성공에 이르기까지의 역사 증거이며 다시 실행할 지시가 아니다.
#### 역사 기록: 첫 clean commit preflight — include 52 / exclude 6
2026-08-09 최종 경로 목록 SHA-256은 ordinal 정렬된 UTF-8 LF 경로를 줄바꿈으로 연결하고 마지막 개행 없이 계산해
include `4d94694ccf9def6945272c61f673cac0eaa9fb59f4f9b6c165d661d819f891a1`, exclude
`1ff8c26732772df6b5857bf84de01a53d7da9c3b7d52df08938e77f3a527f022`였다. 아래 목록은 첫 clean commit 범위를
감사하기 위해 보존한 이력이며 현재 worktree staging 목록이 아니다.
```text
apps/api/app/config.py
apps/api/app/routes/voice.py
apps/api/app/services/session_metrics.py
apps/api/app/services/voice.py
apps/api/app/session_read_model.py
apps/api/app/test_session_read_model.py
apps/api/app/test_voice_service.py
apps/api/app/test_voice_ws.py
apps/api/requirements.txt
apps/web/e2e/admin.spec.ts
apps/web/e2e/dev-dashboard.spec.ts
apps/web/e2e/self-directed-learning-loop.spec.ts
apps/web/e2e/session-mvp.spec.ts
apps/web/e2e/session-persistence.spec.ts
apps/web/e2e/session-review-fixture.ts
apps/web/e2e/session-review.spec.ts
apps/web/index.html
docs/HANDOFF.md
docs/TODO.md
docs/decisions/local-voice-stack.md
docs/dev_dashboard.html
docs/guides/local-development.md
docs/guides/testing.md
docs/ops/backlog-2026-06-26.md
docs/ops/evidence/g8-clean-head-material-milestone-2026-08-09.json
docs/ops/nas-preview-g8-rollback-proof-runbook.md
docs/ops/outcome-os-g7-external-proof-readiness-2026-08-07.md
docs/ops/public-runtime-watchdog.md
scripts/boot-public-runtime.ps1
scripts/capture-g7-topology-evidence.py
scripts/check-dev-dashboard-ssot.py
scripts/check-g7-external-proof.py
scripts/install-public-runtime-task.ps1
scripts/local-whisper-stt-server.py
scripts/melotts-server.py
scripts/probe-public-voice-sidecars.py
scripts/register-boot-task.ps1
scripts/run-g7-external-proof-window.py
scripts/run-outcome-os-release-agent.py
scripts/start-local-whisper-stt.ps1
scripts/start-public-runtime.ps1
scripts/test_g7_external_proof.py
scripts/test_g7_topology_evidence.py
scripts/test_local_whisper_stt_server.py
scripts/test_melotts_server.py
scripts/test_outcome_os_release_agent.py
scripts/test_public_runtime_watchdog_provenance.py
scripts/test_public_voice_sidecar_probe.py
scripts/test_run_g7_external_proof_window.py
scripts/test_start_public_runtime_contract.py
scripts/watch-public-runtime-hidden.vbs
scripts/watch-public-runtime.ps1
```
아래 6개와 ignored `infra/.env.nas-preview`는 절대 stage하지 않는다. NAS 실행 시 env는
`--nas-env-file D:\workspace\vignette\infra\.env.nas-preview`로 외부 주입한다.
```text
apps/api/engine.public.err.log.2026-08-07T2200.bak
apps/api/engine.public.err.log.20260809-160042.bak
apps/api/engine.public.out.log.2026-08-07T2200.bak
apps/api/engine.public.out.log.20260809-160042.bak
apps/web/test-results/g7-voice-consent-desktop.png
apps/web/test-results/g7-voice-consent-mobile.png
```
#### 2026-08-09 clean-head 실행 결과 — NAS mutation 0
- dry-run `D:\workspace\vignette-recovery\g8-clean-head-dry-run-20260809.json`: `ok=true`,
active `6030a677…c611`, desired archive `6b1b15bd…a3f7`, `deployment_mutated=false`.
- execute 1 `...\g8-clean-head-execute-20260809.json`: Windows Git archive의 CRLF 때문에 생성 API 타입 byte check가
실패했다. archive SHA 검증 후 candidate의 생성 타입/셸 스크립트만 LF로 정규화하도록 controller를 수정했다.
NAS mutation 0, rollback 불필요.
- execute 2 `...\g8-clean-head-execute-20260809-r2.json`: API/types/typecheck/build/candidate stack은 통과했고
Playwright는 **109 passed / 5 failed**였다. source-only UUID 스펙 4건은 production build에서
`/src/lib/uuid.ts`를 import할 수 없는 gate 배치 오류라 별도 Vite gate로 분리했다. 남은 1건은 아래 제품 버그다.
NAS mutation 0, rollback 불필요. 동일 전체 execute를 아직 다시 시도하지 않았다.
#### 2026-08-09 clean-head `5221f79e` 실행 결과 — NAS mutation 0
- exact include 52개를 Yun Chan으로 커밋했다. HEAD `5221f79e…1c69`, tree `e4f15001…308b`, 2회 동일 archive
`1109bf86…0f4b`(482,662,400 bytes)를 detached clean worktree에서 고정했다.
- 새 milestone과 dry-run은 active `6030a677…c611` → desired `1109bf86…0f4b`,
`deployment_mutated=false`로 통과했다. 실행 직전 NAS custom dump
`6f4b95a7…b529f`(1,015,222 bytes, TOC 1,752 / TABLE DATA 129, 전용 pgdata volume)를 생성했다.
- execute는 API 921, API types, typecheck, build, insecure-context 6/6, candidate stack과 DB-backed
single-run 후반부까지 통과했지만 **110 passed / 2 failed**에서 승격 전에 중단됐다. 실패는 제품 화면이 아니라
`React 부트가 실제로 비어 있으면 진단을 유지한다`의 desktop/mobile 두 복제였다.
- 원인은 테스트가 Vite 개발 entry `/src/main.tsx`만 차단해 production candidate의 hashed entry
`/assets/index-*.js`를 차단하지 못한 환경 계약 누락이다. 두 entry를 함께 차단하도록 수정했고 Vite source 2/2와
production preview 2/2를 각각 통과했다. 전체 execute를 즉시 반복하지 않고 이 수정의 새 clean commit·archive를
다시 결속한 뒤 한 번만 재실행한다.
- 후속 clean commit `21461ab3…6fde`, tree `5c5f12a8…524e`, 2회 동일 archive `20d49694…fa1a` execute는
candidate **112/112**를 통과하고 NAS 새 API/Web image `d5021950…`/`376a3aa8…`까지 올렸다. 그러나 실제 NAS-origin
postdeploy의 G4 deliberate-practice desktop/mobile 2건이 110/112에서 실패해 active-state commit 전에 이전
`52e0e816…`/`6fdbb646…`로 자동 rollback했고 health·auth 401·OpenAPI 126·G0~G8 route와 image ID를 재검증해
rollback `verified`로 종료했다.
- focused exact-image 평문 origin에서 요청이 API까지 가지 않고 일반 오류로 끝나는 것을 재현했다. 원인은
문장 원문을 보내지 않기 위한 fingerprint가 `crypto.subtle.digest`에만 의존해 insecure origin에서 예외가 난 것이다.
원문 외부 전송 없이 portable SHA-256 fallback을 추가하고 표준 digest
`c06db0f0…62aa`를 payload에서 exact 검증했다. localhost secure origin 2/2와 실제 Tailnet insecure origin 2/2가
같은 digest로 통과했다. 이 수정의 새 clean commit만 다시 결속해 전체 execute를 재개한다.
#### P0 수정 완료 — 회기 timestamp UTC 전송과 KST 표시 날짜
- 최초 실패 스펙: `session-persistence.spec.ts``persists AI tutor coaching history through reload @single-run`.
- 코칭 생성·DB 저장·reload 후 `C` 마커까지는 성공했지만, 수정 전 화면이 `9:03:59 / 543:59` 경과로 계산해
`회기 시간이 종료됐어요` 모달을 띄웠고 그 모달이 코칭 마커 클릭을 가로막았다.
- 원인: `apps/api/app/session_read_model.py::iso()`
`datetime.fromtimestamp(ts).isoformat(timespec="seconds")`로 timezone 없는 문자열을 반환한다.
`apps/web/src/pages/Session.tsx::elapsedFromSession()``Date.parse(detail.started_at)`로 이를 브라우저 로컬 KST로
해석한다. Linux API UTC와 KST 브라우저 사이에서 약 9시간 오차가 생긴다.
- 수정: `session_read_model.iso()`와 dashboard용 `session_metrics.iso_datetime()`은 UTC `+00:00`을 반환한다.
회기 리뷰 달력 날짜는 서버 timezone이 아니라 고정 KST(+09:00)를 사용한다.
- backend 관련 65 passed, 후속 voice 계약을 포함한 API 전체 **921 passed**, Ruff·compile·web typecheck가 통과했다.
- 고유 Compose project/volume의 실제 DB/API/engine/browser에서 KST context로 focused 1/1 PASS했다. UTC suffix,
`status=active`, `ended_at=null`, 회기 나이 10분 미만, 화면 `00:00-09:59`, timebar/dialog 0,
coach mark 실제 클릭과 DB history/source-pack dialog를 모두 검증했다. 종료 뒤 container/volume/listener는 0이다.
- **테스트 강제 클릭이나 모달 닫기 우회는 사용하지 않았다.** 첫 clean source `5221f79e…1c69`에는 제품 수정과
controller 변경이 포함됐고, production entry 차단 회귀 2건의 테스트 수정만 후속 clean commit으로 다시 고정한다.
### 6.2 G8: NAS preview 배포와 실제 origin E2E — DONE
전용 NAS preview만 대상으로 백업→dry-run→배포→검증했고 공개 DB·공개 API는 건드리지 않았다. 아래 성공 조건을
전부 충족했다.
최종 fresh backup은
`/volume1/docker/vignette-preview-20260807/backups/vignette-preview-pre-clean-head-retry-61a41d1f-20260809T112038Z.dump`,
SHA-256 `36ec8748cf28c520d9489d108dee9d4a154e6260f6996b4da3e0fe3b8a624db8`,
1,097,100 bytes, TOC 1,738 / TABLE DATA 129다. release agent 자체에는 `pg_dump`/`pg_restore` 실행 계약이 없다.
따라서 **매 execute 직전** 별도 승인된 백업 단계에서 fresh custom dump를 만들고 SHA-256·byte size·TOC parse/count와
current DB identity를 증거에 결속한 뒤에만 승격한다. 기존 dump 존재만으로 이 단계를 생략하지 않는다.
release agent의 자동 rollback 범위는 exact API/Web image, Compose 적용 상태, active-state 파일뿐이다. 이미 적용된
DB migration이나 데이터 변경은 자동 복구하지 않는다. migration/data restore는 별도 owner 승인·대상 DB identity·dump
hash를 갖춘 복원 절차로만 수행한다. 이미지 rollback 성공을 DB rollback 성공으로 기록하면 안 된다.
- active archive `4d15d055…119d4d`, exact API/Web `d5021950…e4b1`/`9796c092…4b36`, fresh dump SHA,
previous `6030a677…c611` 보존.
- health ok, db/engine true, auth 401, OpenAPI 126.
- `http://100.116.83.60:8088` **실제 평문 NAS origin**에서 11-spec browser review 112/112.
- `crypto.randomUUID` 예외 0, error boundary 0, SSE→DB review PASS.
- 학생 홈→회기→리뷰→G4/G5 반환 연습의 desktop/mobile·DB read reload·멱등·UUID 비노출 유지.
- 앞선 실패 시 자동 rollback과 previous image 복구를 검증했고 최종 성공 뒤 G8 카드를 DONE으로 바꿨다.
### 6.3 G7: 외부 종료 Gate
G7 내부 소스 계약은 완료됐고 `scripts/check-g7-external-proof.py`는 현재 의도적으로 exit 1이다.
네 artifact를 **같은 public host, 겹치는 50분 시간창**으로 수집해야 한다.
운영·코드 선행조건과 무마이크 rehearsal은 닫혔다.
- 공개 API는 detached-clean `a73bcd24…`·tree `b02b3a1b…`에서 OpenAPI 126과
`/admin/voice-runtime`을 제공한다. `/voice/health``local_whisper/small` +
`melotts/melotts-korean`, Uvicorn WS queue 4를 정확히 보고하고 receipt `47388d58…9b08`가 passed다.
- local Whisper 9882와 MeloTTS 9883은 API보다 먼저 exact readiness를 통과했고, 런처는 기존 비정상 리스너를
임의 종료·재사용하지 않는다. public health는 promotion과 rehearsal 뒤에도 `status=ok·db=true·engine=true`다.
- **외부 실행 전 코드 P0 3건은 완료:** (1) production 프로세스 exit 0은 canonical checker
`returncode == 0 && gate_closed == true`에 결속한다. (2) 실제 브라우저 Origin
`https://vignette.chanpaca.net`은 exact HTTPS allowlist로, API/WSS/admin/topology
`api-vignette.chanpaca.net`은 별도 transport host와 `wss`/`https` scheme으로 검증한다. (3) 실제 capture 기본·최소를
3,120초로 올리고 sampler를 ceil+terminal sample로 계산하며, checker가 voice/runtime/topology 공통 교집합
**3,000초 이상**을 강제한다. current six-suite 87/87과 runtime sampler 4/4를 통과했다.
- 현재 공개 topology는 Windows host Uvicorn + cloudflared다. 명시적 `windows_host` topology mode는 구현 완료됐다.
API/cloudflared의 PID·start time·executable name/SHA256·command-line SHA256·Git SHA·cwd를 pin하고, 각 sample 전후
identity와 Git HEAD drift를 검사한다. RSS/peak RSS/CPU/handles/threads, process TCP/established/listener,
API listen-port owner/conflict와 host TCP high-water도 수집한다. 기존 `linux_compose`의 api+caddy·fresh-container
규칙은 그대로 유지한다.
- Windows topology는 API/cloudflared의 PID·start·exe SHA·command SHA·cwd와 detached-clean commit/tree,
runner/collector/checker SHA, `psutil==6.1.1`을 결속한다. 30초 rehearsal에서 API listener owner/conflict와
cloudflared TCP, RSS/CPU/handles/threads를 7회 수집했고 raw command line·endpoint·UUID·email은 저장하지 않았다.
- watchdog·로그온 boot 두 task도 같은 `a73bcd24…` stable root에 재등록했다. commit/tree/script hash marker,
working directory, `LastTaskResult=0`·Ready를 확인했다. 실제 Windows 재부팅 뒤 복구 smoke는 아직 남아 있다.
- 운영 Python에 `psutil`이 없으면 collector는 `command_unavailable:psutil`로 fail-closed한다. current
`apps/api/requirements.txt`와 명시적 Python 3.11 모두 `psutil==6.1.1`로 고정됐다.
- 현재 운영 STT 모델은 이 호스트에서 실측된 CPU int8 `small`로 고정한다. cuDNN 9가 설치되고 별도 성능·정확도
gate를 통과하기 전까지 launcher·API runtime metadata·50분 runner/checker expected model을 모두 `small`로 유지한다.
- human voice-gain pack은 category와 categorical κ를 필수로 포함하고, preregistration이 held-out 공개보다 앞서야 하며,
양 조건이 모두 관측된 50회기/150축만 paired 표본으로 집계한다. 한쪽 결측이면 양쪽 모두 최대오류 ITT로 처리한다.
`scripts/check-g7-human-voice-gain.py --input <pack.json>`을 먼저 통과하지 못하면 production runner는 마이크를
열기 전에 exit 2로 끝난다. 스키마는 `--print-schema`로 출력한다.
- 실제 평가팀 intake는 `scripts/prepare-g7-human-voice-gain-intake.py --create-template <dir>`로 시작한다. 생성물은
header-only이고 `template_only=true`라 증거가 아니다. provenance와 비식별 participant/labeler/observation CSV를
채운 뒤 `--compile <dir> --out <pack.json>`을 실행하면 외부 report의 ICC·κ와 행 재계산값의 일치를 확인하고
production gate가 전부 통과할 때만 기존 파일을 덮어쓰지 않고 최종 pack을 만든다. 콘솔은 경로·키·라벨·잘못된
셀 값을 반사하지 않는다.
runtime/checker 6파일 94건, intake 6건, standalone validator 4건을 합쳐 관련 계약 104/104를 통과했다.
빈 템플릿 행동 검증은 `template_is_evidence=false`, compile exit 1, `pack_written=false`를 반환했다. 실제 사람 데이터는 아직 없다.
- 무마이크 rehearsal 산출물은
`D:\workspace\vignette-runtime-evidence\g7-rehearsal-b34623f3db05-20260809T134105Z`에 있다.
voice `61af2e98…009c`, runtime `c6e8670e…4454`, topology `89f1cb86…a251`이며 세 leg 모두 passed,
`physical_capture=false`다.
```powershell
& $py -X utf8 -B scripts/check-g7-external-proof.py `
--voice-soak <soak.json> --runtime <runtime.json> `
--topology <topology.json> --human-voice-gain <pack.json>
```
1. `scripts/soak-public-voice-websocket.py` v4 — 운영 기본값 `local_whisper`/`melotts`의 ready metadata가
실제 provider/model과 정확히 일치하는 authenticated public WSS,
**명시 동의 물리 마이크** 50분 양방향 `passed`. `--confirm-physical-capture` 없이는 장치 열거·캡처를 하지 않는다.
2. `scripts/capture-g7-runtime-evidence.py` — 같은 시간창의 관리자 endpoint worker/Uvicorn queue high-water.
3. `scripts/capture-g7-topology-evidence.py` — 같은 host·exact image의 50분 cgroup/proc/Docker/TCP high-water.
4. 독립 blind human voice-gain pack — held-out 30명 외 calibration split 참가자 포함 총 최소 31명 /
held-out 50회기 / 150 paired axis / blind evaluator 2인 /
ICC(A,1) ≥ 0.75 · κ ≥ 0.70 · gain ≥ 0.01 · participant-cluster bootstrap 10,000회 95% CI lower > 0.
준비 상세는 `docs/ops/outcome-os-g7-external-proof-readiness-2026-08-07.md`.
G7 Windows topology 회귀 명령:
```powershell
Set-Location D:\workspace\vignette
$env:PYTHONPATH='D:\workspace\vignette\apps\api'
& $py -X utf8 -m unittest scripts/test_g7_external_proof.py scripts/test_g7_topology_evidence.py `
scripts/test_g7_runtime_evidence.py scripts/test_g7_external_voice_soak.py `
scripts/test_run_g7_external_proof_window.py apps/api/app/test_g7_voice_gain_evidence.py
& $ruff check scripts/capture-g7-topology-evidence.py scripts/check-g7-external-proof.py `
scripts/run-g7-external-proof-window.py scripts/test_g7_topology_evidence.py `
scripts/test_g7_external_proof.py scripts/test_run_g7_external_proof_window.py
```
**사용자에게 받아야 하는 것 (코드로 대체 불가):**
- 물리 장치 선택과 3,120초 실행에 대한 **명시적 동의**
- 실제 참가자·독립 평가자 운영 승인
합성 label이나 무동의 mic probe로 대체하지 않는다. `check-g7-external-proof.py` exit 0 전에는 G7 메인 상태를
DONE으로 바꾸지 않는다.
## 7. 학생 자기주도 학습 폐루프
```text
학습자 홈 추천 → 새 회기/사전 설정 → 실제 Session → 종료 리뷰 → G4 처방 또는 G5 전이 의도
→ 별도 재연습 회기 → completed-session observer → 실제 G4 attempt / G5 transfer execution
→ read-model reload → before/after·진행도·멱등 재확인
```
실제 DB-backed browser 증거: `apps/web/e2e/returned-practice-db-closed-loop.spec.ts` +
`apps/web/e2e/harness/prepare-returned-practice-db.py`, desktop 2/2 + mobile 2/2, route mock 0,
visible raw UUID 0, getUserMedia/enumerateDevices 0, horizontal overflow 0.
이 harness는 **disposable clone**(전용 DB container + 전용 API 포트 + 전용 vite 포트)에서만 실행한다.
공개 런타임 포트를 재사용하지 않는다. `--api-base-url`, `--database-url`, `--practice-internal-token`,
`--transfer-internal-token`이 필요하고 API health의 `db`·`engine`이 모두 true여야 한다.
2026-08-07 재실행 절차(그대로 재현 가능):
1. `docker run -d --name vignette-g8-e2e-db-<날짜> -p 127.0.0.1:55439:5432` + `infra/db/init` 마운트,
`POSTGRES_USER=vignette_owner` / `APP_DB_USER=vignette_app`. `app.ci_regression_dag_node`가 생기면 준비 완료.
2. 별도 엔진 게이트웨이를 **새 포트**(예: 9199)에 띄운다. 상주 게이트웨이의 claude 세션이 죽어 있으면
`engine=false`가 되므로 공개용 9099를 재사용하지 않는다.
3. uvicorn API를 8021에, vite를 5199에 띄우고 `ENGINE_URL`을 2번 게이트웨이로 지정한다.
4. harness 실행 → `E2E_RETURNED_PRACTICE_DB_CLOSED_LOOP=1` + `E2E_RETURNED_PRACTICE_FIXTURE`로 spec 실행.
5. 결과 기준: desktop 2/2 + mobile 2/2 = **4 passed**. 끝나면 컨테이너·프로세스를 모두 정리한다.
G4/G5 핵심 production bug 수정은 보존한다.
- G4 runtime SQL `digest(...)``app.digest(...)`
- G5 JSONB bind는 `json.dumps` string이 아니라 dict/list object 전달
- route는 nested Pydantic suite를 `body.model_dump()` dict로 깨지 않고 typed object로 전달
## 8. 최종 검증 명령과 현재 기준선
```powershell
$py = 'C:\Users\encep\AppData\Local\Programs\Python\Python311\python.exe'
$ruff = 'C:\Users\encep\AppData\Local\hermes\hermes-agent\venv\Scripts\ruff.exe'
```
### Backend full — 기준 API **921 passed**, gateway **58 passed**
```powershell
Set-Location D:\workspace\vignette\apps\api
& $py -X utf8 -B -m pytest -p no:cacheprovider app -q
& $py -X utf8 -B -m pytest -p no:cacheprovider engine_gateway -q
```
### G8 executor/probe/helper/release governance — 기준 28 / 11 / 37 / 30
```powershell
Set-Location D:\workspace\vignette
& $py -X utf8 -B -m unittest scripts/test_serve_nas_preview_rollback_executor.py
& $py -X utf8 -B -m unittest scripts/test_probe_nas_preview_g8_rollback.py
& $py -X utf8 -B -m unittest scripts/test_launch_nas_preview_g8_helpers.py
& $py -X utf8 -B -m pytest -p no:cacheprovider scripts/test_outcome_os_release_agent.py -q
& $ruff check scripts/serve-nas-preview-rollback-executor.py scripts/test_serve_nas_preview_rollback_executor.py scripts/probe-nas-preview-g8-rollback.py scripts/test_probe_nas_preview_g8_rollback.py scripts/launch-nas-preview-g8-helpers.py scripts/test_launch_nas_preview_g8_helpers.py scripts/run-outcome-os-release-agent.py scripts/test_outcome_os_release_agent.py
```
### Web contract/type/build
```powershell
Set-Location D:\workspace\vignette\apps\web
npm run check:api-types
npm run typecheck
npm run build
```
### 현재 HEAD 학생 UX·접근성 focused — 기준 **20/20**
2026-08-09 읽기 전용 재감사에서 `check:api-types`·typecheck·build와 아래 fixture-only 묶음이 desktop/mobile
20/20을 통과했다: Alliance Pulse 8, 자기주도 폐루프 2, insecure-origin UUID 6, pre/mid 자기점검 2,
시간만료 경고·종료 모달 2. 390×844·320×568 overflow 0, tab 키보드, Enter CTA, 44px 조작부,
4.5:1 대비, raw UUID/theory key 비노출, 동의 전 `getUserMedia`/`enumerateDevices` 0도 확인했다.
추가 회귀에서 새 음성 동의 modal을 실제로 수락한 뒤 위기 음성 종료까지 가는 desktop/mobile, 모바일 review
filter 44px 계약을 desktop/mobile로 검증해 4/4 통과했다. dashboard의 file-origin은 spec-relative repo root로
고정했고, session-review fixture 소유권과 viewport assertion, admin live snapshot race를 교정했다. 16:49 단일 실행은
**120/120 PASS(92.0s)**였지만, 그 뒤 dashboard 변경분은 dashboard 10/10으로만 따로 통과했다. 따라서 현 트리는
110 불변 + 10 최신 분할 GREEN이고, 최종 clean source에서 아래 exact 묶음을 다시 단일 실행해 HEAD/tree/archive
결속 뒤 120/120을 요구한다.
#### current-source fixture exact 120 — clean HEAD 재현 명령
정확한 수집 구성은 `dev-dashboard` 10 + `session-mvp` 24 + `session-review` 24 + `learner` 14 +
`session-layout` 8 + `admin` 40 = desktop/mobile 120이다. `127.0.0.1:8000`은 반드시 복구 dump에서 만든
**disposable API/DB/engine 전용 스택**이어야 한다. 이 묶음은 dev-login·세션 쓰기를 포함할 수 있으므로 public
8001, 복구 원본 55432, candidate DB, NAS DB에 연결하지 않는다.
```powershell
$ErrorActionPreference = 'Stop'
[Console]::OutputEncoding = [Text.UTF8Encoding]::new($false)
$OutputEncoding = [Text.UTF8Encoding]::new($false)
$cleanRoot = 'D:\workspace\vignette-clean-<NEW_SHA>'
$cleanWeb = Join-Path $cleanRoot 'apps\web'
$tempRoot = Join-Path $env:TEMP 'vignette-clean-fixture-120'
$tempWeb = Join-Path $tempRoot 'apps\web'
$tempEvidence = Join-Path $tempRoot 'docs\ops\evidence'
if (Test-Path -LiteralPath $tempRoot) { throw "기존 TEMP와 충돌: $tempRoot" }
$pw = Join-Path $cleanWeb 'node_modules\.bin\playwright.cmd'
if (-not (Test-Path -LiteralPath $pw -PathType Leaf)) {
throw 'clean worktree에서 npm ci --ignore-scripts를 먼저 실행해 node_modules를 준비할 것'
}
$health = Invoke-RestMethod -Uri 'http://127.0.0.1:8000/health' -TimeoutSec 5
if ($health.status -ne 'ok' -or -not $health.db -or -not $health.engine) {
throw 'disposable API8000의 db/engine ready가 아님'
}
New-Item -ItemType Directory -Path $tempWeb -Force | Out-Null
New-Item -ItemType Directory -Path $tempEvidence -Force | Out-Null
$config = Join-Path $cleanWeb 'playwright.config.ts'
$files = @(
'e2e/dev-dashboard.spec.ts',
'e2e/session-mvp.spec.ts',
'e2e/session-review.spec.ts',
'e2e/learner.spec.ts',
'e2e/session-layout.spec.ts',
'e2e/admin.spec.ts'
)
$env:PLAYWRIGHT_PORT = '15374'
$env:VITE_API_PROXY_TARGET = 'http://127.0.0.1:8000'
Remove-Item Env:PLAYWRIGHT_BASE_URL -ErrorAction SilentlyContinue
Remove-Item Env:PLAYWRIGHT_SKIP_WEB_SERVER -ErrorAction SilentlyContinue
$code = 99
try {
Push-Location $tempWeb
try {
& $pw test @files ("--config={0}" -f $config) `
--project=chromium-desktop --project=chromium-mobile `
--workers=4 --reporter=line ("--output={0}" -f (Join-Path $tempRoot 'pw-results'))
$code = $LASTEXITCODE
} finally {
Pop-Location
}
} finally {
$listeners = @(Get-NetTCPConnection -State Listen -LocalPort 15374 -ErrorAction SilentlyContinue)
$listenerError = $null
if ($listeners.Count -ne 0) { $listenerError = "Playwright Vite listener 잔존: $($listeners.OwningProcess -join ',')" }
if (Test-Path -LiteralPath $tempRoot) {
$resolved = (Resolve-Path -LiteralPath $tempRoot).Path
if ($resolved -ne $tempRoot) { throw "unexpected TEMP target: $resolved" }
[IO.Directory]::Delete($tempRoot, $true)
}
if ($listenerError) { throw $listenerError }
}
if ($code -ne 0) { throw "clean fixture 120 failed: $code" }
```
과거 API-ready 없이 돌린 별도 Vite-only 혼합 실행의 `session-layout` 8건은 DB persona가 없어 제품 assertion 전에
`seed_fallback/degraded`로 중단됐다. 위 exact 120 명령은 이 사각지대를 닫기 위해 disposable API의 db/engine health를
선행 강제한다. 최종 완료 때는 `session-layout` 8/8과 returned-practice DB closed-loop 4/4를 모두 다시 통과시킨다.
### Dashboard SSOT + E2E — 기준 SSOT PASS(FAIL 0), unit 5/5, dashboard 10/10
```powershell
Set-Location D:\workspace\vignette
& $py -X utf8 -B scripts/check-dev-dashboard-ssot.py
& $py -X utf8 -B -m pytest -p no:cacheprovider scripts/test_dev_dashboard_ssot.py -q
Set-Location D:\workspace\vignette\apps\web
.\node_modules\.bin\playwright.cmd test e2e/dev-dashboard.spec.ts --project=chromium-desktop --project=chromium-mobile --workers=1 --reporter=line
```
status count를 바꾸면 `scripts/check-dev-dashboard-ssot.py``EXPECTED_STATUS_COUNTS`
`apps/web/e2e/dev-dashboard.spec.ts``metrics.done`/`metrics.doing`**함께** 갱신한다.
### 격리 NAS 런타임 회기 E2E
```powershell
Set-Location D:\workspace\vignette\apps\web
$env:PLAYWRIGHT_BASE_URL='http://100.116.83.60:8088'
$env:PLAYWRIGHT_SKIP_WEB_SERVER='1'
node.exe .\node_modules\@playwright\test\cli.js test e2e/session-layout.spec.ts e2e/session-persistence.spec.ts `
e2e/self-directed-learning-loop.spec.ts e2e/alliance-pulse.spec.ts e2e/outcome-trajectory.spec.ts `
e2e/rupture-repair.spec.ts e2e/deliberate-practice.spec.ts e2e/calibration-transfer.spec.ts `
e2e/supervision-research.spec.ts e2e/multimodal-alliance.spec.ts e2e/continuous-improvement-admin.spec.ts `
--project=chromium-desktop --project=chromium-mobile --project=chromium-single-run --workers=1 --reporter=line
```
전체 Playwright inventory는 629 tests / 45 files다. 실행 환경/API/DB를 정확히 맞추지 않고 fixture failure를
제품 failure로 오인하지 않는다. 같은 blocker가 두 번 반복되면 전체 재시도 대신 원인·증거·수정 계획을 먼저 보고한다.
주기 실회기 검증은 `scripts/run-periodic-learner-e2e.py`가 소유한다. NAS 112건 중 실 API/DB는 22건이고
route fixture는 90건이므로 전체 숫자를 실제 회기 폐루프로 부르면 안 된다. 새 runner는 clean HEAD/tree에서만
전용 engine/DB/API/Web을 만들고 같은 학습자의 SSE→review→G4/G5 0→1→1과 returned-practice desktop/mobile 4건을
검증한 뒤 sentinel resource·PID·listener·temp 잔여 0을 영수증으로 남긴다. 공개 8001/55432/9099와 NAS는 금지다.
최초 세 full run은 SSE body replay, durable turn 전 종료, 비종료 SSE `response.finished()` 대기를 차례로 찾아 모두
fail-closed했다. 4차 clean run은 reload 뒤 persisted episode를 `학습자만 실행`으로 표시하는 제품 결함을 검출했다.
5·6차 clean run은 핵심 동일 학습자 SSE·review·G4/G5 `0→1→1`을 연속 통과했지만 fixture 공유와 mobile success-state locator를 각각 fail-closed했다. 7차 clean `aa81af29…`는 첫 전체 GREEN을 만들었다. `94666192…``f97e7fad…` run은 disposable DB init에서 멈췄고 두 번째 receipt의 cleanup 전 로그가 `99_app_role.sh``#!/usr/bin/env bash\r` exit 127을 확정했다. Git blob LF를 Windows checkout에서도 보존하도록 `.gitattributes``*.sh text eol=lf`를 고정했다. 공통 톱바 desktop/mobile 44px을 포함한 clean `b02bee26…`·tree `8f677bba…`의 최신 single full run은 487.3초에 GREEN이었다. receipt `periodic-learner-e2e-20260812-171547.json`·SHA-256 `8c11a136…af8e3`은 same-learner SSE·review·G4/G5 `0→1→1`, returned desktop/mobile 4/4, route mock 0, public/NAS/active DB 접촉 0과 exact cleanup 0을 증명한다. heartbeat는 최신 GREEN이 6시간 이상 오래됐거나 material milestone이 바뀔 때만 다시 실행한다.
## 9. 핵심 변경 파일
- `scripts/launch-nas-preview-g8-helpers.py` · `scripts/test_launch_nas_preview_g8_helpers.py` (신규, Gate6 계약)
- `scripts/serve-nas-preview-rollback-executor.py` · `scripts/test_serve_nas_preview_rollback_executor.py`
- `scripts/probe-nas-preview-g8-rollback.py` · `scripts/test_probe_nas_preview_g8_rollback.py`
- `scripts/run-outcome-os-release-agent.py` · `scripts/test_outcome_os_release_agent.py`
- `scripts/check-dev-dashboard-ssot.py` · `scripts/test_dev_dashboard_ssot.py`
- `scripts/check-g7-external-proof.py` · `scripts/soak-public-voice-websocket.py` ·
`scripts/capture-g7-runtime-evidence.py` · `scripts/capture-g7-topology-evidence.py` ·
`scripts/run-g7-external-proof-window.py`와 대응 G7 테스트 3개
- `apps/web/src/lib/uuid.ts` · `apps/web/e2e/insecure-context-uuid.spec.ts` (신규, 비-secure origin 결함 수정)
- `apps/api/app/session_read_model.py` · `apps/api/app/services/session_metrics.py` ·
`apps/api/app/test_session_read_model.py` · `apps/web/e2e/session-persistence.spec.ts` (UTC/KST P0와 실DB browser 회귀)
- `apps/web/index.html` · `apps/web/e2e/self-directed-learning-loop.spec.ts` (SPA route 전환 중 boot diagnostic 회복)
- `apps/web/e2e/admin.spec.ts` · `apps/web/e2e/dev-dashboard.spec.ts` ·
`apps/web/e2e/session-review-fixture.ts` · `apps/web/e2e/session-review.spec.ts`
- `scripts/watch-public-runtime.ps1` · `scripts/boot-public-runtime.ps1` ·
`scripts/install-public-runtime-task.ps1` · `scripts/register-boot-task.ps1` ·
`scripts/watch-public-runtime-hidden.vbs` · `scripts/test_public_runtime_watchdog_provenance.py`
- `apps/web/e2e/returned-practice-db-closed-loop.spec.ts` ·
`apps/web/e2e/harness/prepare-returned-practice-db.py`
- `docs/dev_dashboard.html` · `docs/TODO.md` · `docs/README.md` · `docs/ops/backlog-2026-06-26.md`
- `docs/ops/nas-preview-g8-rollback-proof-runbook.md` ·
`docs/ops/evidence/nas-preview-g8-actual-rollback-2026-08-07.json` ·
`docs/ops/nas-preview-deployment-evidence-2026-08-07.md`
## 10. 최종 원칙
- source-only PASS를 runtime DONE으로 부르지 않는다.
- health만 보고 배포 완료라고 하지 않는다. auth, OpenAPI, assets, browser SSE→DB review를 함께 본다.
- synthetic control plane을 실제 NAS rollback으로 과장하지 않는다.
- G7 물리 마이크·사람 평가를 무동의/합성 데이터로 대체하지 않는다.
- current/previous images와 DB backup을 확인하기 전 destructive operation을 실행하지 않는다.
- 성공보다 정직한 fail-closed 증거가 우선이다.
## 11. 권장 멀티에이전트 실행 구조
사람 개발자 작업계획은 만들지 않는다. 구현·테스트·배포·증거 동기화는 에이전트가 수행하고, 사람에게는 코드로
대체 불가능한 동의·인증 세션·실제 참가자/평가자 승인만 요청한다. 동시 작업은 최대 세 하위 lane으로 나누되,
NAS/public/DB mutation owner는 항상 한 에이전트만 둔다.
1. **G7 proof lane:** 완료된 clean public runtime과 무마이크 rehearsal을 기준선으로 보존하고, 장치 선택·명시 동의 뒤
3,120초 mic/runtime/topology 동시 캡처와 human pack·canonical checker를 닫는다.
2. **G8/NAS recovery lane:** 완료된 archive·images·rollback 증거는 보존한다. current relay는 source-pinned launcher
8/8과 detached-clean `f08e03de…f191` exact check-only까지 완료됐고 실제 9100은 0 listener다. 소유자 승인 전
mutation 0; 승인 뒤 같은 source/tree·hash 결속으로 actual 1회→NAS-origin 재검증.
3. **E2E/SSOT lane:** local fixture, disposable DB browser, visual/accessibility, dashboard/TODO/backlog 정합을 읽기 전용으로 감사.
루트 에이전트는 각 lane의 증거 SHA와 실패 원인을 합쳐 게이트를 판정한다. 같은 blocker가 두 번 반복되면 세 번째
재시도 전에 원인·증거·수정 계획을 사용자에게 보고한다.
## 12. 새 에이전트에 그대로 줄 시작 프롬프트
```text
D:\workspace\vignette의 docs/HANDOFF.md를 인수인계 SSOT로 읽고, AGENTS.md → docs/README.md →
docs/dev_dashboard.html → docs/TODO.md → docs/ops/backlog-2026-06-26.md 순서로 현재 상태를 재확인해.
G8 배포 source는 HEAD `61a41d1f…6af`·tree `87dec55d…3b77`·archive `4d15d055…119d4d`다. 격리 NAS의
API/Web `d5021950…e4b1`/`9796c092…4b36`, candidate 112/112, actual NAS-origin 112/112, 당시 health·auth 401·OpenAPI
126을 통과했고 fresh dump `36ec8748…24db8`과 previous `6030a677…c611`을 보존했다. 이 종료 증거는 DONE이지만
current NAS는 engine relay 부재로 DEGRADED이고 2026-08-12 15:24 health 3회가 타임아웃했다. 실제 relay 복구는
소유자 승인 전 금지한다. 현재 공유 worktree의 후속 변경은 이 배포 source와 분리해서 다뤄.
최종 목표는 G0~G8 아홉 목표를 코드-only가 아니라 실제 runtime 증거로 모두 닫는 것이다. G0~G6과 G8은 DONE이다.
G7은 내부 구현과 clean public runtime·local_whisper/melotts ready·authenticated public WSS 무마이크 rehearsal까지 완료했다.
이제 사용자가 고른 장치와 명시 동의를 받은 물리 마이크 3,120초 soak, 같은 시간창의 runtime/topology high-water,
독립 human voice-gain pack을 모아 canonical checker exit 0을 만들어야 한다.
G7 runner exit의 checker exit 0/gate_closed 결속, browser Origin과 API/WSS host 분리, 3,120초 capture와
3,000초 공통 overlap, detached-clean HEAD/tree, runner/collector/checker SHA와 exact psutil pin은 소스에서 완료됐다.
API/cloudflared는 detached-clean `a73bcd24…`·tree `b02b3a1b…`에서 새 PID로 교체됐고 safe receipt
`47388d58…9b08`가 passed다. 공개 OpenAPI 126·`/admin/voice-runtime`·local provider/model·queue 4와
public health db/engine true를 확인했다. 30초 rehearsal은 인증 WSS·runtime·Windows topology 세 leg 7 samples를
모두 통과했고 `physical_capture=false`·UUID/email 0이다. 이 증거를 물리 마이크나 사람 평가로 과장하지 마.
watchdog·로그온 boot도 같은 stable root와 exact Git commit/tree/script SHA에 재등록했고 두 task의 명시 실행 결과가
0·Ready다. 실제 Windows 재부팅 뒤 자동복구 smoke는 별도 운영 게이트로 남는다. cwd/Git pin이나 기존 Linux Compose
gate를 약화하지 말고 합성 증거로 G7을 DONE 처리하지 마.
학생 자기주도 폐루프는 홈 추천→회기→리뷰→G4 처방/G5 전이→별도 재연습→completed-session observer→
actual attempt/execution→read-model reload→before/after·멱등까지 desktop/mobile 실제 DB/browser로 검증해.
최종 검증은 API full, gateway full, schema/provenance, Web API types/typecheck/build, fixture E2E, disposable DB E2E,
NAS-origin E2E, visual/accessibility, auth/OpenAPI/assets/SSE→DB review를 포함해야 한다.
독립 작업은 서브에이전트로 병렬화하되 NAS/public/DB mutation은 단일 owner만 수행해. 공개 DB 복구 volume과
pre-recovery container/dumps, NAS named volume과 previous images는 삭제하지 마. 비밀/PII/raw UUID를 증거에 남기지 마.
과거 로컬 recovery dump는 현재 위치를 별도로 확인하기 전 삭제로 단정하거나 그 파일을 전제로 복원하지 마.
2026-08-09 fresh public dump `vignette-dev-db-vignette-20260809-122235Z.dump` SHA `f1fd569c…5f64`도 보존해.
source-only PASS나 health-only를 DONE으로 부르지 말고, 대시보드·TODO·backlog·HANDOFF를 마지막 증거와 함께 동기화해.
모든 게이트가 실제로 닫히기 전에는 goal complete를 선언하지 마.
```

View file

@ -0,0 +1,13 @@
# 2026-09-07 문서 정리 전 원문 보존
이 폴더는 활성 문서를 현재 실행 항목으로 줄이기 전에 복사한 원문이다. 현재 상태의 권위 기준으로 사용하지 않는다.
| 파일 | 보존 이유 |
|---|---|
| [`HANDOFF-before-cleanup.md`](./HANDOFF-before-cleanup.md) | 2026-08-18까지의 상세 인수인계와 검증 이력 |
| [`TODO-before-cleanup.md`](./TODO-before-cleanup.md) | 완료·진행·외부 게이트가 함께 있던 통합 TODO 원문 |
| [`backlog-2026-06-26-before-cleanup.md`](./backlog-2026-06-26-before-cleanup.md) | 완료된 A 경로·B3 결정 기록을 포함한 백로그 원문 |
| [`handoff-goal-production-2026-08-29.md`](./handoff-goal-production-2026-08-29.md) | 2026-08-29~30 배포 전 인수인계 기록. 2026-09-01 NAS 배포 뒤 현행 실행 근거에서 분리했다. |
| [`refactor-governance-2026-07-15.md`](./refactor-governance-2026-07-15.md) | 완료된 P1~P8 리팩터 감사·검증 이력. 후속 리팩터는 활성 TODO·backlog가 소유한다. |
원문은 삭제하거나 내용을 정정하지 않았다. 2026-09-01 배포 당시 증거는 [NAS 배포 증거](../../ops/evidence/nas-prod-deploy-2026-09-01.json)를 참조한다.

View file

@ -0,0 +1,659 @@
# Vignette 할 일 (안된 것들) — 통합 TODO
> **한 곳에서 보는 "남은 작업" 인덱스.** 여러 문서에 흩어져 있던 열린 작업을 주제별로 모았다.
> **권위 기준은 SSOT `dev_dashboard.html`**, 열린 게이트 추적은 `ops/backlog-2026-06-26.md`(얇은 현행본)다.
> 이 파일은 그 둘을 **집계·링크**하는 뷰이며, 상태가 어긋나면 대시보드를 기준으로 맞춘다.
> 완료된 작업 기록은 `archive/`(냉동 보관)에 있고 평상시 읽지 않는다.
>
> 태그: **[환경]** 이 워크스테이션에서 증거 생산 불가 · **[외부]** 기관/소유자 서면·결정 필요 ·
> **[구현]** 코드로 진행 가능 · **[임상]** 임상팀(구훈정·어유경) 외부 콘텐츠 소유.
---
## 개선관리 워크북 동기화 (2026-08-29)
> 이 표는 워크북 ID와 현행 구현을 잇는 얇은 추적표다. 내부 기술 DONE의 focused 증거는
> `guides/testing.md`, 상세 계약은 SSOT `dev_dashboard.html`이 소유한다. C-001은 기술 사전검증과 외부
> 임상 승인을 분리한다. 검토 패킷과 fail-closed 판정기는 준비됐지만 외부 증거가 없으므로 계속 열린 항목이다.
> 완료본은 `outputs/01a04217-f73b-7303-b597-401fa7f5d290/Vignette_개선관리_완료.xlsx`, SHA-256
> `c832547f30ae0664e54b302c8e9f62cc157f31022ad158d17803d8cac988d8bd`다. 집계는 **완료 9·검토 2·총 11**이며
> 검토는 C-001 적격 외부 임상 검수와 REQ-008 실제 Windows 재부팅뿐이다.
| ID | 상태 | 현행 경계 |
|---|---|---|
| C-001 | **OPEN · 외부 GATE** | v2 P1 합성 사례 6건·상태별 canonical 판정기와 109·수단 상세 차단·런타임/carry-over/output `ideation_stage <= 3`·4개 공식 source 기술 사전검증은 완료. 사례별 5필드 30개, 청소년 답변 6개, 검토자·소속·자격·검토일·결정 근거·서면 증거/해시·검토 버전/사례 해시를 포함한 승인 10필드가 있어야 닫는다. 현재 `review_complete=false`; F절/B4에서 추적한다. |
| C-002 | **DONE · 내부 기술** | 첫 회기 라포 반영·한 초점 열린 질문 체크리스트, 근거 turn 연결, 이후 회기 not-applicable |
| C-003 | **DONE · 내부 기술** | 종료 회기 4건 미만 insufficient, 이후 최다 페르소나 비중 0.75 이상 훈련 집중 주의; 임상·공정성 판정 아님 |
| REQ-001 | **DONE · 운영 수락** | provider가 검증한 모든 Google 계정은 도메인·사전등록 없이 learner·approved로 로그인한다. Pages `0c60261e…`의 단일 Google CTA·공개 Playwright 2건, 기존 Gmail 인증 세션의 로그인 이메일·관리자 진입·온보딩 비전환·복구 데이터 가시성을 확인했고 소유자가 수락했다. 현재 public API 장애는 별도 운영 복구 게이트이며 이미 닫힌 수락 증거를 지우지는 않는다. |
| REQ-002 | **DONE · 내부 기술** | admin protocol draft→active→retired, 라이선스 C/D 외부 LLM 금지, evaluator-only RAG·rollback; migration 17 owner-run/readiness-only |
| REQ-003 | **DONE · 내부 기술** | 페르소나 주호소가 JSON 키 순서나 surface 상태보다 우선 |
| REQ-004 | **DONE · 내부 기술** | learner 현재 설정 AND 세션 snapshot. OFF는 AI 파생 API 403/UI 무요청·혼합 결과 redaction, 사용자 입력·privacy 보존; teacher/admin 유지 |
| REQ-005 | **DONE · 내부 기술·운영 실증** | 새 회기는 승인 페르소나 `persona_id/version` 고정. P20의 `suicide_ideation_stage=0.9 → int(0) → DB 1..5` 장애를 clamp로 제거했고, 공개 P20 회기 생성·실제 AI 응답·종료·평가를 확인 |
| REQ-006 | **DONE · 내부 기술** | prompt와 구조화 결과의 상담자/내담자 identity를 `[COUNSELOR]`/`[CLIENT]`로 역할 마스킹 |
| REQ-007 | **DONE · 내부 기술** | 회기 종료를 평가보다 먼저 영속화해 평가 실패 뒤에도 같은 learner-persona case의 다음 회기 생성·턴 진행 가능. 종료·시작은 같은 case row lock 순서를 써 S2가 S1 summary/carry를 빠뜨리지 않고, 진행 중인 같은 case는 409 `active_session_exists`로 기존 회기만 이어간다. migration 21의 online partial unique index와 invalid-index pre/postcondition이 병렬 생성을 fail-closed로 차단 |
| REQ-008 | **READY · 실제 재부팅 GATE** | 원본 응답 생성 실패는 기본 엔진+live-client readiness, 구조화 done 없는 SSE EOF의 `client_stream_incomplete` 복원과 공개 P20 실제 응답으로 닫았다. 후속 재부팅 복구는 전역 mutex·단계별 상한·프로세스 트리 종료·전체 다운 즉시복구와 PowerShell 5.1 timed `WaitForExit`의 process handle 선확보를 적용했다. 실제 PS5.1 계약 32 passed, detached-clean `44b7835c…`·tree `06133249…`, 5174 자동복구·HTTP 200, watchdog result 0/failcount 0까지 확인했지만 실제 Windows 재부팅 smoke 전에는 개선관리 상태를 `검토`로 유지한다. PC 비종속 상시 호스트 이전도 별도 운영 게이트다. |
2026-08-29 read-only 상태(<b>이 PC 로컬 기준</b>): 정적 public Web과 Pages `0c60261e…`(source `5bf89ff…`)는 유지됐지만,
이 PC 로컬 API 8001은 연결 거부였고 이 PC의 Docker daemon 부재로 이 PC 기준 DB/API는 복구 불가였다. task는 old
detached `dba9b75a…` runtime을 계속 가리켰다. <b>2026-08-31 재실측(정식 분리 확인):</b> `https://api-vignette.chanpaca.net/health`
별도 호스트(Cloudflare 뒤)에서 200·`environment=prod`·`db=true`·`engine=true`·`engine_mode=openai`로 정상 서빙 중이며,
이 PC에는 cloudflared 프로세스가 없어 public 도메인은 이 PC가 아니다. 즉 이 PC 로컬 8001/task 상태는 개발 워크스테이션의
잔여 스냅샷이고 public 도메인 장애를 뜻하지 않는다. avatar fail-closed 코드와 upload receipt는 아직 후보이고 배포하지 않았다.
2026-08-30 NAS production 재확인에서는 `vignette-prod` DB·API·engine이 healthy였고, restart loop이던 web/proxy는
필요 최소 capability만 추가해 두 서비스만 재생성했다. 내부 Caddy→web과 `/api/health`는 200이지만, compose의
Tailnet bind 주소가 NAS 인터페이스에 없어 외부 private/public GREEN은 아직 주장하지 않는다. LAN/0.0.0.0으로
노출을 넓히지 않았다. 회기 연속성 migration 21은 중복 활성 learner-persona case 14개(초과 row 28개)를 발견해
보존·종료 기준을 owner가 결정할 때까지 적용하지 않는다. 기존 20번 transactional migration과 새 21번 online index,
연속성 hunk만 포함한 production 전용 clean candidate가 필요하며 preview 전용 release agent와 8/7 manifest는 사용하지 않는다.
---
## A. 배포·공개 런타임 게이트 [환경]
> 출처: `ops/backlog-2026-06-26.md` B2 · 대시보드 운영 탭 · `ops/tailscale-vnet-runtime-2026-06-27.md` · `ops/public-runtime-watchdog.md` · `ops/handoff-goal-production-2026-08-29.md`
>
> **2026-09-01 정식 배포(A 경로) 완료:** Forgejo master `dce85620`를 NAS `vignette-prod`에 실배포(api/engine/web 교체·pgdata/apiuploads 무접촉·사전 dump `cab6826b…` 보존), 중복 활성 회기 30행 최신 유지 정책 종료 후 마이그레이션 20/21/22 온라인 적용, Cloudflare Pages도 새 빌드 배포. public health `ok·db/engine true`·OpenAPI 200·auth 401·dev-login 404·데이터 집계 전후 동일. 이 절의 "이 PC 런타임" 항목들은 NAS 이관으로 대체 경로가 됐다. 증거: `ops/evidence/nas-prod-deploy-2026-09-01.json`, 상세는 백로그 A 경로 절.
- [ ] **public API/DB 복구와 avatar fail-closed 정식 승격** — 허용 source union은 93 objects·52,973 bytes,
inventory SHA-256 `9d703126f78d4fc8330408835d76a7d680276240dc578d6fc9ca420c2f25e6aa`다. 실제 decode는 정상 3·실패 90,
현 DB 참조는 정상 2·실패 6·missing 0이다. 후보는 손상 bytes/DB reference를 private forensic으로 보존하고
public static 응답을 404/fallback으로 막으며 decode-valid 객체만 immutable public cache에 제공한다. 신규 UUID 업로드도
URL 기록·현재 참조·GET/HEAD에서 다시 full decode하고, 업로드 뒤 손상은 current invalid/health fallback에 포함한다. 전체 API/Web/
PowerShell 회귀, 실제 브라우저 fallback, exact target/total/digest receipt, 후보 SHA와 사용자 배포 승인 전에는 Docker·
API/tunnel·task·DB avatar URL·stable upload root를 바꾸지 않는다.
현재 Docker engine은 꺼져 있고 public watchdog이 활성이라 daemon 시작 즉시 기존 runtime 복구가 개입할 수 있다. 풀스택 E2E는
watchdog 일시 중지·비활성화, 고유 DB/container/volume과 56432/58000/55173 포트, exact cleanup, Docker·watchdog 원상복구를
명시 승인받은 뒤 `layout-visual-gate` 15/15와 `session-layout` 8/8을 실행한다.
- [ ] **내장 브라우저 G8 사람 게이트 E2E** — 명시적 승인·보류(`keep_quarantine`)·반려(`reject`)와 사유 입력,
증거 미충족 승인 차단은 typecheck와 route-fixture desktop/mobile **10/10**을 통과했다. 빠른 3.5초/느린 8초 응답에서도
watchdog overlay 0을 확인했다. Codex 내장 브라우저의 격리 local stateful fixture에서도 8.5초 뒤 overlay 0·heading 정상,
content `keep_quarantine` 뒤 effect 0, 증거 없는 release 승인 disabled→`reject` 뒤 effect 0, 증거 4종 promote 승인 뒤
lifecycle effect 정확히 1을 공동 확인했다. <b>로컬 사람 게이트는 GREEN</b>이지만 실DB append-only 결정과 public proof는
아직 없으므로 current 운영 G8 전체를 GREEN으로 올리지 않는다.
- [ ] **공개 DB 계정·회기 복구 안정화** — 2026-08-07 18:26 KST owner 승인으로 recovered named volume을
공개 `vignette-dev-db:55432`에 전환했다. cutover 기준 users 84/sessions 30/turns 705,
Google 계정 16/소유 회기 30, orphan 0, health·engine·OAuth·watchdog가 정상이다. 전환 직전 container와
dump, 원본 volume을 보존한다. 남은 것은 사용자가 Google 재로그인한 뒤 본인 과거 회기가 보이는지
확인하는 일과 자동 백업 운영화다. 기존 인증 세션은 복사하지 않아 재로그인이 필요하다.
2026-08-09 Docker Desktop 중단과 18:02:55 KST Docker Desktop UI의 active DB 직접 stop으로 `db=false`
재발했으나 동일 recovered container/volume만 재기동했고, 당시 owner read-only 집계는 users 356/sessions 193/
turns 726, Google users 16/Google-owned sessions 31이며 public db/engine true였다. watchdog·로그온 boot는 detached
clean release의 Git commit/tree와 watchdog|boot/start SHA가 없으면 mutation 전에 실패하도록 코드로 고정했다.
공개 API·cloudflared는 detached-clean `a73bcd24…`·tree `b02b3a1b…`로 fresh 승격했고 receipt
`47388d58…9b08`가 passed다. local/public health는 `status=ok·db=true·engine=true`, 공개 OpenAPI 126,
`/admin/voice-runtime`, `local_whisper/small` + `melotts/melotts-korean`, queue 4를 확인했다. 로그온·watchdog
두 task도 같은 root/commit/tree/script SHA에 재등록해 명시 실행 `LastTaskResult=0`을 확인했다.
2026-08-12에는 휴면 Gradle daemon의 `8001` 점유·cloudflared/voice sidecar 중단과 PowerShell 5.1 native stderr
조기 종료 결함을 복구했다. DB mutation 전에 custom dump `9cbda31e…0ce5`(7,532,625 bytes·TOC 1,492)를
고정했고, detached-clean `a73bcd24…`·tree `b02b3a1b…`로 API/cloudflared를 fresh 승격했다. receipt
`47388d58…9b08` passed, public health 3/3·db/engine true·OpenAPI 126·local voice exact, watchdog·로그온 boot
명시 실행 result 0을 확인했다.
2026-08-29에는 Windows PowerShell 5.1에서 `Start-Process -PassThru`의 process handle을 열기 전에 timed
`WaitForExit()`를 호출하면 정상 web build 뒤에도 `ExitCode=$null`로 남아 복구를 실패로 오판하는 경계를 수정했다.
실제 PowerShell 5.1 계약 32 passed, detached-clean `44b7835c…`·tree `06133249…`에 두 task를 같은 핀으로
재등록했다. watchdog 3회차 복구로 5174가 자동 기동했고 후속 실행은 `LastTaskResult=0`·failcount 0,
local/public health `status=ok·db=true·engine=true`다. 실제 Windows 재부팅 smoke는 아직 남아 있다.
2026-08-18에는 06:30~10:05 KST 엔진 readiness 장애(고효율 프로브 타임아웃 실패가 1800초 캐시 +
구독 사용량 소진)를 수정했다. 실패 전용 캐시 TTL 30초 분리·프로브 타임아웃 45초로 교정하고
detached-clean `4771b97c…c880`으로 API/게이트웨이/task를 재승격·재등록해 `LastTaskResult=0`,
공개 health ok·OpenAPI 126을 재확인했다. 승격 절차에 이전 루트 `apps/api/.env` 복사 단계가
필요함을 확인했다(1차 승격 fail-closed·약 4분 다운타임).
남은 자동복구 gate는 실제 Windows 재부팅 smoke와 정기 DB 백업 운영화다.
상세: `ops/public-db-recovery-rehearsal-2026-08-07.md`.
- [ ] **자동 DB 백업·정기 복원 리허설**`scripts/backup-vignette-db.ps1`의 custom dump→TOC 검증→
SHA manifest 원자 게시와 현재 public-target 일회용 복원(129 tables/9,646 rows/orphan 0)을 통과했다.
남은 범위는 예약 실행, 실패 알림, NAS/off-host 암호화 복제, 주기적 restore drill과 보존 정책이다.
- [x] **공개 Google OAuth 실제 `/turn` proof** — 2026-08-28 기존 Google 세션으로 callback→관리자 진입을
확인하고 P20 회기 생성→자기점검 잠금→실제 학습자 발화/AI 내담자 응답→종료→리뷰·평가→다음 회기 버튼
재노출까지 공개 브라우저 폐루프를 통과했다. 별도의 신규 Gmail 선택은 새 Vignette 계정 생성 확인 항목이다.
- [x] **공개 Web 모바일 보정 Pages 배포** — 2026-08-18 소유자 승인 후 detached-clean `cf899b7f…7c96`
완료했다(00f851a6 후보의 모바일 보정을 포함한 상위 커밋). API type/typecheck, 2회 결정적 build(599파일
sha256 전일치), 7개 origin 자산 143개 보존(최종 742파일) 후 Cloudflare Pages production `4edc02d3` 배포 —
custom-domain 새 엔트리 `index-BWxBaCPM.js`/`index-BUfQc2Rm.css` 서빙, 직전 엔트리 `index-BrpoQ5nP.js` 200
보존, preview 200을 확인했다. 공개 API/DB·Scheduled Task는 건드리지 않았다. 증거:
`docs/ops/evidence/public-web-deploy-2026-08-18.json`.
증거: `ops/evidence/public-web-preflight-2026-08-12.json`.
- [ ] **Compose 패키징용 `infra/.env` owner-secret** — 현재 운영 direct-runtime은 독립 G3~G8 token 주입,
non-owner app-role preflight와 공개 health를 통과했다. Compose로 전환할 때만 `APP_DB_PASSWORD` 등 패키징
비밀과 prod-safe engine/voice flags를 별도 주입하고 compose profile preflight를 다시 통과한다.
- [ ] **`vnet.18ka.net`/`api-vnet.18ka.net` 공개 live** — DNS A record 등록, cloudflared의 `18ka.net` zone 권한,
Google redirect URI `https://api-vnet.18ka.net/auth/callback` 등록. (현재 prod API 8001은 health OK)
- [x] **비-secure origin에서 회기 리뷰 크래시 수정** — 2026-08-07 격리 NAS 프리뷰
`http://100.116.83.60:8088`(평문 HTTP·비-localhost)에서 회기 리뷰 라우트가
`crypto.randomUUID is not a function`으로 error boundary에 잡혔다. `crypto.randomUUID`는 secure
context에서만 노출되는데 제품 코드 18곳이 fallback 없이 직접 호출했고, 특히
`RuptureRepairCard.tsx`는 렌더 시점(`useRef`)에 호출해 화면 전체가 죽었다. 릴리스 게이트는
localhost 후보 스택(secure context)에서만 돌아 이 경로를 밟은 적이 없다. `src/lib/uuid.ts`
`randomUuid()`로 통일했고 fallback도 `crypto.getRandomValues`를 우선 사용해 idempotency key의
예측 불가능성을 유지한다. 문장 fingerprint의 `crypto.subtle.digest` 의존도 portable SHA-256 fallback으로
교체했다. source-only UUID/SHA 회귀 12/12와 secure/insecure exact digest 회귀를 고정했고, 최종 archive
`4d15d055…119d4d`의 실제 NAS 평문 origin browser review 112/112로 배포 검증까지 닫았다.
- [x] **워치독 engine readiness 사각지대** — 2026-08-07 22:0x KST 소유자 승인으로 9099 재기동과
판정 기준 교정을 모두 마쳤다. 복구 실측: 9099 `/ready?force=true` 200 `ok:true, detail:OK`,
API 8001과 공개 `api-vignette.chanpaca.net` 모두 `status:ok, engine:true, engine_detail:OK`.
원인은 16:06부터 상주하던 9099 **프로세스 하나**였다. 같은 인자로 `claude -p`를 직접 실행하고
`EngineSession`을 그대로 재현하면 양쪽 다 `"OK"`를 반환해 CLI·코드·cwd는 무관함을 확인했다
(별도 포트 9199 실험과 동일 결론).
재발 방지 3건:
(1) `scripts/watch-public-runtime.ps1`·`scripts/start-public-runtime.ps1`의 engine 판정을
`/health`(프로세스 liveness) → `/ready`(실제 생성)로 바꾸고, API 판정에 `engine`을 추가했다.
shared secret 인스턴스를 401로 오판하지 않도록 `X-Vignette-Engine-Token` 헤더를 붙이고,
503 응답 **본문**을 로그 detail에 남긴다.
(2) `engine_gateway/gateway.py`가 자식 `claude -p`의 stderr를 상시 드레인해(PIPE를 아무도 읽지
않으면 자식이 블록되는 문제도 함께 차단) 실패 detail에 `exit`·stderr를 붙인다. 실증:
`empty engine response (error: unknown option '--vignette-nonexistent-flag-xyz')`.
`str()`이 비는 `TimeoutError` 등에는 타입명을 최소 보장한다.
(3) 기동 시 게이트웨이 로그 회전, `Stop-UvicornByPort``Name -like python*` 조건 추가
(명령 문자열만 매칭해 호출자 자신을 죽이는 사고 방지).
검증: `pytest engine_gateway` 58/58, 워치독 `-CheckOnly` healthy 5/5, 장애 재현(9299를
`CLAUDE_BIN` 부재로 기동)에서 `/health``ok:true`로 통과하고 `/ready`는 503으로 검출되어
워치독이 `unhealthy (1/3)` + 503 본문을 남겼다.
NAS용 9100은 2026-08-12 listener 상실을 확인했다. current relay launcher는 detached-clean HEAD/tree,
launcher·Python·env SHA, exact consumer URL, explicit LAN 승인, 외부 receipt/runtime, listener owner,
generation readiness와 실패 cleanup을 강제하도록 보강했고 contract 8/8을 통과했다. detached-clean
`f08e03de…f191`·tree `00275133…0abd` exact `-CheckOnly`도 launcher/Python/env SHA 결속,
`mutation=false`, listener `0→0`, runtime state 0으로 PASS했다. 실제 9100 시작은 소유자 승인 전 미실행이다.
- [x] **NAS preview engine relay current 복구** — 2026-08-18 심야 current GREEN 달성으로 종결.
`-CheckOnly` 프리플라이트(4종 SHA pin) → 실제 시작 passed → 방화벽(LAN 한정 9100) → health 3/3
`status=ok·db=true·engine=true`·auth 401·OpenAPI 126 → **actual-origin 실회기 turn→review smoke 통과**
(agy 턴 2개→종료 200→리뷰 200). 대청소 사건으로 릴레이 루트가 삭제돼 현행 루트 `bf5f7352`로 재핀(r3 영수증)했고,
DHCP가 LAN IP를 224로 회수해 릴레이 바인딩·NAS `ENGINE_URL``192.168.0.224:9100`으로 이전(방화벽 규칙 재발행,
NAS `.env(0600)` 재구성 + api만 재생성), 클로드 예산 한도 재발으로 NAS `ENGINE_MODE=agy_cli` 전환. 잔여 권고:
공유기 DHCP 예약(호스트 MAC D8-BB-C1-A2-3C-5D), Docker VM 클럭 동기화(호스트 대비 ~4일 선행),
NAS 로컬 agy 이전(릴레이·노트북 의존 제거). 증거: `docs/ops/evidence/nas-relay-recovery-2026-08-18.json`.
- [ ] **stable-source task 재등록 + 재부팅 후 watchdog smoke** — 2026-08-27 stale AF_UNIX socket으로
Docker Desktop 백엔드와 공개 DB가 중단된 사건을 기존 container·volume 삭제/교체 없이 복구했다. 부모 IPC 폴더는
timestamp `.stale` 이름으로 보존했고, 기존 `vignette-dev-db``vignette_recovered_prod_20260807`만 재기동했다.
2026-08-28에는 detached-clean `99779a6a…fbc8`·tree `57da86aa…6b91`로 watchdog·로그온 boot task를
다시 등록했다. 두 작업 모두 `ExecutionTimeLimit=PT1H`·`MultipleInstances=IgnoreNew`, scheduler 재시도 0이며,
launcher 전역 mutex·bounded API/voice/web build·부모/자식 process-tree 종료와 cloudflared 또는 전체 동적
서비스 hard-down 즉시복구를 회귀 50건으로 고정했다. action root/commit/tree/script SHA pin, watchdog
18:10:57 KST·boot 18:14:27 KST 수동 실행 `LastTaskResult=0`, failcount 0을 확인했다. 로그인 뒤 수동 복구는
증명됐지만 실제 Windows 재부팅 후 엔진/API/터널 자동 복구는 아직 실측하지 않았다. DNS 개통 후 `api-vnet.18ka.net`
`-AdditionalPublicHealthUrls`로 명시 추가한다.
- [x] **노트북 로컬 음성 스택 STT 구현** — 2026-08-08 소유자 결정으로 음성 양쪽을 노트북 상주
로컬 모델로 쓴다(STT faster-whisper, TTS는 아래 항목대로 MeloTTS). 이전에는 코드에 로컬 STT 경로가 아예 없었고
(`voice_stt_provider``openai|deepgram` 둘뿐), interim/final 스트리밍 구현도 Deepgram 전용이었다.
`scripts/local-whisper-stt-server.py` 사이드카(Higgs와 같은 loopback 상주 방식)와
`voice_stt_provider=local_whisper` 경로를 추가했다. 오디오는 호스트를 벗어나지 않고 외부 STT 키가
필요 없다. 실측: 저장소 합성 시드 음성 8.72초로 interim 9 · final 5 · word timestamp 12개 전부 present,
전사는 원문과 한 글자(`안녕하세요``안녕하세여`) 차이였다. 회귀 `scripts/test_local_whisper_stt_server.py`
37/37, `apps/api/app/test_voice_service.py` 로컬 provider 케이스와 후속 계약을 포함한 current API 전체 921 passed.
**발견 1:** 이 노트북은 CUDA 장치는 보이지만 cuDNN(`cudnn_ops64_9.dll`)이 없어 GPU 추론이 **네이티브
크래시**로 죽는다. 같은 프로세스 try/except로는 못 잡아서 디바이스 확인을 버릴 수 있는 자식 프로세스
(`--self-check`)로 분리하고 `auto`에서 CPU로 폴백하게 했다. cuDNN 9를 설치하면 GPU(float16)로 훨씬
빨라지며 `large-v3`도 실시간권에 들어온다 — 설치는 소유자 판단 사항이라 하지 않았다.
**발견 2:** G7 종료 체커가 `expected_stt_provider == "deepgram"`을 하드코딩하고 있었다. 이는 결정
기록이 아니라 벤더 한 줄이었으므로, 운영하기로 한 provider 허용목록으로 바꿨다
(`ALLOWED_STT_PROVIDERS`/`ALLOWED_TTS_PROVIDERS`). 선언/실제 불일치 차단은 그대로 유지한다.
- [x] **노트북 로컬 음성 스택 TTS 구현 (MeloTTS)** — Higgs는 연구/비상업 라이선스라 `config.py`
운영에서 차단하고 있었고, 그 가드를 푸는 건 법적 판단이라 코드로 결정할 수 없었다. 그래서 상업 사용이
허용된 설치형을 다시 찾아 **MeloTTS Korean(MIT)** 으로 바꿨다. 가드를 건드릴 필요가 사라졌다.
`scripts/melotts-server.py` loopback 사이드카 + `voice_tts_provider=melotts` 경로를 추가했고,
사전학습 다화자 모델이라 Higgs처럼 P1 프리셋 한정이 아니라 모든 페르소나에 적용된다.
**실측:** CPU 정상 상태 RTF 0.27~0.28(실시간의 3.6배), `POST /tts` 200·WAV 350,566 bytes·3.61s,
빈 텍스트 422·미지 경로 404. **왕복 검증**으로 MeloTTS 합성음을 faster-whisper가 완전 일치 전사했다.
회귀 사이드카 16/16, 후속 계약을 포함한 current API 전체 921 passed, ruff clean.
**검토 후 탈락:** Kokoro-82M은 Apache 2.0이지만 공식 `VOICES.md`**한국어가 없다**(검색 요약이
지원한다고 했으나 오답). Piper는 GPL, XTTS-v2·Fish Speech는 비상업이라 Higgs와 같은 문제다.
설치 함정 3가지는 `decisions/local-voice-stack.md`에 남겼다(setuptools<81, unidic download, eunjeon).
근거: [`decisions/local-voice-stack.md`](./decisions/local-voice-stack.md).
- [ ] **G7 외부 종료 게이트 — 공개 runtime 선행 구현 + 사람 입력** — provider 의존 코드는 해소됐다. 2026-08-08 로컬 스택
(faster-whisper STT + MeloTTS TTS, 둘 다 MIT)으로 바꿔 외부 STT 키가 더 이상 필요 없고, 체커의 벤더
하드코딩도 운영 provider 허용목록으로 교정했다. 기계적인 부분도
`scripts/run-g7-external-proof-window.py`가 soak·runtime·topology 세 캡처를 **하나의 겹치는 시간창**에서
동시에 돌리고 checker까지 이어서 실행하도록 묶었다(회귀 20/20).
사람에게서만 받을 수 있는 마지막 두 입력은 다음이다.
(1) **명시 동의 하 물리 마이크 50분 양방향 soak** — 소유자가 실제로 50분 발화해야 한다.
(2) **독립 blind human voice-gain pack** — held-out 30명 외 calibration split 참가자 포함 총 최소 31명 /
held-out 50회기 / 150 paired axis / 독립 평가자 2인 /
ICC(A,1) ≥ 0.75 · κ ≥ 0.70 · gain ≥ 0.01 · participant-cluster bootstrap 10,000회 95% CI lower > 0.
runner는 이 pack의 category·κ·preregistration·paired completeness·결측 ITT를 3,120초 캡처 전에 검증하며,
부적합하면 마이크를 열지 않고 exit 2로 끝낸다. 독립 검수는
`python -X utf8 -B scripts/check-g7-human-voice-gain.py --input <pack.json>`이고 `--print-schema`로 계약을 확인한다.
독립 평가팀용 `prepare-g7-human-voice-gain-intake.py`는 빈 header-only 템플릿을 만들고, 비식별 CSV를
production floor·ICC·κ·gain·CI로 재계산해 전부 통과한 경우에만 최종 pack을 원자 생성한다. 템플릿은
`template_only=true`라 증거로 컴파일할 수 없고 이름·이메일·원음·축어록·자유서술 열을 받지 않는다.
도구 준비만 끝난 상태이며 실제 독립 평가자 데이터는 아직 없으므로 G7 external GATE를 유지한다.
**공개 선행 조건 완료(2026-08-09):** detached-clean `a73bcd24…`·tree `b02b3a1b…`에서 OpenAPI 126,
`/admin/voice-runtime`, exact local voice provider/model과 queue 4를 제공한다. fresh launcher receipt
`47388d58…9b08`가 passed이고 두 Scheduled Task도 동일 source에 pin돼 result 0이다. Cloudflare의 Python
기본 User-Agent 403 경계는 runtime sampler의 browser-compatible header와 회귀 4/4로 고정했다.
**외부 실행 전 코드 P0 완료:** runner 종료 코드는 canonical checker `exit 0`/`gate_closed=true`에 결속됐고,
브라우저 Origin(`vignette.chanpaca.net`)은 API/WSS/admin/topology host(`api-vignette.chanpaca.net`)와 분리된 HTTPS
allowlist로 검증한다. 실제 capture는 최소 3,120초·ceil+terminal sample이고 voice/runtime/topology 공통 overlap
`≥3000s`를 checker가 강제한다. Windows artifact는 detached-clean HEAD/tree, runner/collector/checker SHA,
exact `psutil==6.1.1`을 매 sample 전후 pin한다. fresh launcher는 legacy API와 exact-config cloudflared를 bounded
교체하고 새 PID/start/exe/command SHA/실제 cwd의 raw command-line 없는 receipt를 만든다. G7 runner/checker/topology
human pack runtime/checker 6파일 94/94, intake 6/6, standalone validator 4/4를 합친 104/104를 통과했다. `--rehearse`는 인증 WSS ready/ping/close 1000,
runtime 7 samples, Windows topology 7 samples를 모두 통과했고 마이크 capture false·UUID/email literal 0이다.
fail-closed 경계(동의 없음·pack 없음·3,120초 미만·host/Origin/scheme 불일치)는 CLI로 실증했다. 상세:
`ops/outcome-os-g7-external-proof-readiness-2026-08-07.md`.
> **격리 NAS current baseline / G8 `DONE`:** 2026-08-09 clean-head 프리뷰
> `http://100.116.83.60:8088`, project `vignette-preview-20260807`은 source HEAD `61a41d1f…6af`·tree
> `87dec55d…3b77`의 active archive `4d15d055…119d4d`, exact API/Web `d5021950…e4b1`/
> `9796c092…4b36`로 실행 중이며 previous active `6030a677…c611`과 이전 images도 보존한다.
> `api/web/db/proxy` 4개는 running + `unless-stopped`, volume은 `vignette-preview-20260807_pgdata`다.
> candidate session E2E 112/112와 실제 NAS-origin browser review 112/112(11 specs, desktop/mobile/single-run),
> 당시 health ok·db/engine true·auth 401·OpenAPI 126을 통과했다. 2026-08-12 current runtime은 고정
> `ENGINE_URL=http://192.168.0.223:9100` listener 상실 뒤 degraded였고 15:24 KST health 3회도 타임아웃했다.
> 따라서 G8 배포·rollback 증거는 DONE으로 보존하되 current NAS는 relay 복구와 actual-origin 재검증 전까지
> DEGRADED다. fresh dump `36ec8748…24db8`은 1,097,100 bytes,
> TOC 1,738 / TABLE DATA 129다. release agent는 DB
> dump/restore를 수행하지 않으므로 매 execute 직전 fresh custom dump SHA/size/TOC와 DB identity를 별도 결속한다.
> 자동 rollback은 image/Compose/active-state만 복구하며 적용 migration/data restore는 별도 owner 승인이 필요하다.
> 과거 `6030a677…c611`의 localhost 108/108·평문 origin UUID 24건 실패와 후속 verified rollback은 이력으로 보존한다.
> 매시간 `vignette-e2e` heartbeat 자동화는 ACTIVE다. 공개/NAS 화면과 health는 읽기 전용으로 보여주고,
> 소스 material milestone에서만 전용 disposable 회기 E2E를 실행한다. runner의 clean HEAD/tree·로컬 npipe Docker·
> loopback 동적 포트·sentinel·exact cleanup 계약과 unit 14/14는 완료됐다. 최초 세 full run은 각각 SSE body 재읽기,
> durable turn 전 종료, 비종료 SSE의 `response.finished()` 대기라는 harness 결함을 fail-closed로 잡았다. 4차 clean run은
> 6.3분 안에 동일 학습자 SSE·review ready·G4 원장 0→1까지 통과한 뒤, full reload가 persisted episode를 화면에 hydrate하지
> 못해 이미 기록된 상태를 `학습자만 실행`으로 되돌리는 제품 결함을 잡았다. 네 번 모두 public/NAS/active DB 접촉 0·자원 잔여 0이다.
> 현재 UI는 현재 session+prescription의 persisted episode를 `현재 원장 상태`로 복원하며 route fixture desktop/mobile 6/6을 통과했다.
> 5·6차 clean run은 핵심 동일 학습자 SSE·review·G4/G5 `0→1→1`을 연속 통과했지만 fixture 공유와 mobile success-state locator를 각각 fail-closed했다. 보정 후 7차 clean `aa81af29…`는 첫 전체 GREEN을 만들었다. 이후 `94666192…``f97e7fad…` run의 disposable DB init 실패는 두 번째 receipt의 cleanup 전 로그로 detached `99_app_role.sh` CRLF와 `bash\r` exit 127이 원인임을 확정했다. `.gitattributes``*.sh text eol=lf`를 고정했다. 공통 톱바 desktop/mobile 44px을 포함한 clean `b02bee26…`·tree `8f677bba…`의 최신 single full run은 487.3초에 GREEN이었다. receipt `periodic-learner-e2e-20260812-171547.json`·SHA-256 `8c11a136…af8e3`은 same-learner SSE·review·G4/G5 `0→1→1`, 별도 returned desktop/mobile 4/4, route mock 0, public/NAS/active DB 접촉 0과 exact cleanup 0을 증명한다.
> 증거: [배포 증거](./ops/nas-preview-deployment-evidence-2026-08-07.md), [기계 판독 증거](./ops/evidence/nas-preview-current-deploy-2026-08-07.json), [브라우저 증거](./ops/evidence/nas-preview-live-turn-2026-08-07.png). 이는 G7 물리 마이크·독립 사람 평가 증거를 대체하지 않는다.
- [ ] **claude_cli ↔ Anthropic API live 동일성** — provider 라우팅·모델 탐색·설정 저장 경로는 구현 완료. 남은 게이트는 연구팀/기관 `ANTHROPIC_API_KEY`를 게이트웨이 호스트에 주입한 뒤 같은 프롬프트의 live 응답·계량·오류 표면화를 비교하는 것이다.
## B. 외부 거버넌스 [외부]
> 출처: 백로그 B4 · `ops/hanshin-data-governance-gate.md` · `ops/source-docs-gap-analysis-2026-06-26.md` · `MASTERPLAN.md`
- [ ] **한신대 데이터/SSO 거버넌스 서면 증거** — 공식 문의서 발송·서면 응답 수령, 동의범위 매트릭스, SSO claim map,
privacy review, 미성년 원본 활용동의, 개인정보 처리방침. 로컬 골격은 `ops/hanshin-data-governance-gate.md`.
- [ ] **L1 스택 정합성·저작권 등재 문서 반영** — 신청서/등재 문서에 FastAPI 유지 사유 + 계약 우선 Node 전환 계획 반영.
결정 기록: `decisions/backend-node-transition.md`.
## C. Phase 3 교육용 파일럿 [외부] + [구현 scaffold 완료]
> 출처: 백로그 Phase3 · `phase3/*` · 대시보드 P3 탭. 문서·checker·exporter scaffold는 준비됨.
- [ ] **20명 파일럿 운영** — roster, consent 법무승인(G-P3-02), session completion evidence 수집.
- [ ] **효과성/KPI 측정** — SUS·자기효능감·κ≥0.70/ICC≥0.75·환각률≤3%. 3척도 pre/post 실측 → `kpi_report.json`.
- [ ] **재귀학습 데이터셋 approved 산출**`approved_for_recursive_learning_seed`(consent/PII/IAA/legal 승인) + 승인 export 실행.
- [ ] **개인정보/동의 감사 서명**`phase3/privacy-consent-audit.md` 서명 완료, withdrawal_log 종결.
## D. 소유자 결정 완료(2026-06-30) → 구현 후속 [구현]
> 출처: 대시보드 "윤찬 결정 상세" 탭 · `decisions/` · 백로그 B3. 결정 자체는 전건 확정, 아래는 후속 build.
- [ ] **s2s 2차 비교 PoC 구현·실측**`decisions/voice-s2s-poc.md` 기준표(전사·안전·감사·지연)로 20턴 비교 리포트 → 최종 채택/폐기.
- [ ] **fast-loop 외부 폴백 구현** — 로컬 상주 분류기 우선 + Haiku/Solar 폴백, 전송 가드(데이터주권·로그·동의)·관측.
- [x] **운영 TTS provider 선정·통합** — 운영 경로는 OpenAI API `gpt-4o-mini-tts`로 고정했다. OpenAI
Services Agreement의 고객 애플리케이션·최종 사용자 통합권과 출력 소유 조항을 근거로 기록하고, 공식 TTS
가이드가 요구하는 “AI 생성 음성·사람 음성 아님” 고지를 회기 시작 전과 진행 중 UI에 상시 노출한다. 로컬
Higgs와 sample provider는 무참조 synthetic seed를 쓰는 개발 전용 경로로 계속 분리한다.
- [ ] **재귀학습 fine-tuning 파이프라인** — 동의/데이터셋 게이트 충족 후. few-shot 자동갱신은 선행 가능.
- [ ] **헬스 retention 적용·검증** — raw 90일 / rollup 365일. escalation/그룹 자동화는 미도입 고정.
- [x] **한신대 실사용 피드백 1차 개선팩**`ops/hanshin-feedback-improvement-plan-2026-07-03.md`,
`redteam/hanshin-feedback-plan-redteam-2026-07-03.md`, `redteam/hanshin-feedback-plan-counter-redteam-2026-07-03.md`
기준 P0~P4 1차 완료. 완료 범위: role mapping + client-only stateless history injection, review UI 표시 자연어화,
raw error display mapper, fast/deep 라벨, 명백한 출력 결함 차단, SSE full-response buffer, 교수자 설명 패널.
검증 수치는 SSOT 대시보드만 소유한다. partial stream, client 전용 resident session, fast-loop 사후 저장은
구현 완료했다. 남은 후속은 저장형 fallback/synthetic 계약, fast/deep reconciliation,
임상팀 루브릭/CBT/약물·평가 문구 확정.
## D-2. 전수 E2E 순회 소유자 결정 — **전건 결정 완료(2026-07-27)** → 구현 후속 [구현]
> 출처: `ops/e2e-full-sweep-2026-07-27.md` 발견 결함 로그 · `ops/backlog-2026-06-26.md` B3-신규.
> 결함 12건은 RED→GREEN으로 수정 완료(대시보드 143차). 소유자 결정 7건은 질문 도구로 전건 확정.
- [x] **설정 아바타 변경 UI 구현** — RED→GREEN 완료(full-sweep-settings 4/4, 무회귀 44/44)
- [x] **동의 철회 전체 흐름 구현** — 설정 동의 섹션+철회 2단계 확인+재동의, 실 API round-trip GREEN
- [x] **신규 학습자 기초 우선 추천 구현**`starterPersona()` 난도 오름차순, 무회귀 42/42
- [x] **학생 분석 테이블 가상화 구현**@tanstack/react-virtual, 300행 RED→GREEN, 무회귀 40/40+게이트 4/4
- [x] **저작 모드 좌측 레일 마크업 정리** — 레일 JSX·CSS 삭제, dead-code 게이트 통과
- [x] dev-login 재로그인 자동 재승인 — 결정: 현행 유지(코드 변경 없음, 종결)
- [x] **감정 밸런스 타임라인 차트 구현** — deep `turn_valence`+결정론 파생 폴백, 백엔드 421/421, 라이브 5pt 실측
> 전체 검증 수치·상세는 SSOT 대시보드 144차 노트와 `ops/e2e-full-sweep-2026-07-27.md`가 소유한다.
## E. 코드 품질·리팩터 로드맵 [구현] (비게이트 — 여유 시)
> 출처: `ops/code-quality-research-2026-06-26.md` · `decisions/backend-node-transition.md` next candidates.
> **2026-07-02 실측**: 로드맵 상당수는 이미 해결(turn_runtime·tier 제거·eval_hook 주입 등). 아래는 코드 재확인 기준 현행 상태.
- [x] **Stage enum SSOT 통합**`taxonomy.Stage`를 라벨 단일 정의(SoT)로, `services.state_machine`이 re-export(중복 `class Stage` 제거). **검증: 단일 enum 정체성 확인, 백엔드 pytest 352 passed, 수집 379 import 무결. (2026-07-02)**
- [x] **store `recent_turns`/`masked_turns` Type-2 중복 제거**`recent_turns = masked_turns()[-k:]` 위임으로 단일 소유. **검증: 백엔드 352 passed. (2026-07-02)**
- [x] **화자 한글 라벨 4× 중복 → `taxonomy.speaker_ko_label` 단일 헬퍼** — evaluator/live_coach/memory 프롬프트의 `'상담자'/'내담자'` 삼항 통합. **검증: 헬퍼 단위 + 백엔드 352 passed. (2026-07-02)**
- [x] ~~턴 파이프라인 dedup~~`turn_runtime.py``load_owned_session`/`record_completed_turn`로 이미 추출됨.
- [x] ~~`_iso` 중복 통합~~ — teacher/sessions `_iso` 중복 해소됨(현재 `_iso_text`/`_iso_dt`는 별개 목적 함수).
- [x] ~~tier dead-contract 제거~~ · ~~voice 경로 eval_hook 미주입~~ — gateway tier 제거 + voice eval_hook 주입 완료.
- [x] **Speaker/Role 계약 레벨 — 검토 결과 조치 불요** — DB actor 매핑은 1곳(`session_persistence.py`, 중복 아님), `Role` app↔db는 이미 단일 dict(`DB_ROLE_BY_APP`/`APP_ROLE_BY_DB`), 엔진 `AIRole`은 이미 단일 `Literal`. 실제 중복이던 표시 라벨은 위에서 통합됨 → 별도 추상화 불요. (2026-07-02 실측)
- [x] **`_list_sessions` N+1 제거(1차: state/turns)** — 세션별 `session_state`/`turns` fetch 루프(1+2N) → `= ANY($ids)` 배치 3쿼리 + Python 그룹핑. **검증: 실 DB 동등성 50세션/109턴 불일치 0, 왕복 101→3, 백엔드 352 passed(회귀 테스트 mock 시퀀스 갱신). (2026-07-02)**
- [x] **`_list_sessions` N+1 제거(2차: 턴평가 하이드레이션)** — 상세 뷰에서 세션별 evaluator 연결+평가 로드 루프 → 전 세션 turn_refs 배치(`_hydrate_sessions_turn_evaluations`, 연결 1회). 싱글 헬퍼는 플러럴 위임으로 행위 보존(`_load_turn_evaluations`는 이미 `turn_id = ANY` 배치). **검증: 백엔드 352 passed. (2026-07-02)**
- **E2E 검증(실 스택)**: dev API(8000, 내 코드 반영)+웹+DB에서 `chromium-single-run` **2 passed** — "위기→교수자 대시보드", "학습자 턴→리뷰+교수자 대시보드"(`list_all_sessions` include_turn_evaluation=True로 1·2차 N+1 경로 모두 브라우저 end-to-end). (2026-07-02)
- [ ] fast-loop `eval_hook` **사용 정책 제품 결정** (현재 주입은 됨; 켤지/끌지 확정은 소유자).
- [ ] Node 게이트웨이 구현 시: 라이브 엔드포인트 conformance, `TurnEvaluationPayload` 어댑터 추출, `admin.py` 매퍼 정리.
- [ ] init SQL과 별도의 idempotent DB migration runner 도입 (인프라 추가 — 소유자 결정 권장).
## F. 임상팀 콘텐츠 [임상] (외부 소유)
> 출처: `ops/source-docs-gap-analysis-2026-06-26.md` · 백로그 B0. 코드 구조는 선제 구축, 문안·기준은 외부 정의.
- [ ] **태깅 축어록 4종 후속 게이트**`ops/tagged-data-utilization-2026-07-15.md` §3 소유:
golden set P8~P11, 다회기 스키마 격상, gestalt/dbt 이론모드 신설(소유자 결정), P1~P7 보강,
사례개념화 루브릭 draft, P8~P11 초안 검수·승인(특히 P9 자해 페르소나는 안전장치 실측 선행).
- [ ] C1 사례개념화 **확정 루브릭 콘텐츠** + AI 추출/채점 calibration (현재 scaffold_only).
- [ ] CBT 체인·이론부합 루브릭.
- [ ] **위기개입 프로토콜 임상 승인** — 내부 기술 사전검증은 완료했다. P1 합성 검토 사례 6건과 Draft
2020-12 v2 스키마, `pending_external_review`/`approved`/`conditional`/`rejected` 상태별 canonical fail-closed 판정기,
실제 수련생 위기의 엔진 전 차단·109 연결, 자살 수단 상세 차단, 생성·전이·이전 회기 carry-over·출력의
`ideation_stage <= 3`, SAMHSA·NIMH·NICE·보건복지부 source pack 계약을 고정했다. 생성·스트림 실제 경로
P1 6/6과 C-001 focused 90 passed를 통과했고, 개선관리 완료본에는 사례별 판정·근거·수정 요구·재검토·검토일
30개, 청소년 특화 답변 6개, 외부 승인 10필드를 입력하는 `C-001 외부검토` 시트를 추가했다. 근거는
`data/clinical/`, `scripts/check-clinical-crisis-review.py`,
`docs/ops/clinical-crisis-protocol-review-2026-08-27.md`에 있다. 현재 관찰 패키지·작업트리 해시는 일치하고
기계 게이트는 `pending-valid`지만 외부 입력은 모두 비어 있어 `review_complete=false`다. 실제 검토자 이름·소속
기관·자격·검토일·결정·근거·서명 증거/해시·검토 버전/사례 세트 해시가 기록되기 전에는
`clinical_status=approved`나 개선관리 시트 `완료`로 바꾸지 않는다.
- [ ] 평가 골든셋 콘텐츠.
## G. 운영 후속 [구현] (비차단)
> 출처: 백로그 B1
- [ ] **운영 티켓 자동 분류·처리** — Claude Recipe headless 자동 수정 후보, 관리자 승인 후 이슈 등록·PR/작업 스레드 생성,
처리 결과 audit trail 확장. 자동 수정은 운영자 승인 전까지 실행하지 않는다.
## H. 2026-07-13 한신대 회의 결정 구현 [구현] (파일럿 게이트 — 7월 내)
> 출처: `ops/hanshin-meeting-actions-2026-07-13.md` (요구사항 상세·완료기준은 그 문서가 소유).
> 데드라인: **7월 내 배포 → 7월 말~8월 초 파일럿(대학원생 ~20명)**. 9~10월 연구팀 점진 이관.
- [x] **P1. 회기 종료 시간 기반 전환** — 구현 완료(2026-07-14). 60분 시간 기반(`SESSION_DURATION_MINUTES`) +
10분 전 알람 바 + 시간 만료 정리 유도 다이얼로그 + 유예(+10분) 후 서버 턴 409 거부 + 시작 전 목표 1~4개
선택(`goal_stages` 계약, DDL·런타임 가드 포함 — 회의 권장 2개 수준, 소유자 지시 2026-07-15로 4개까지 허용).
목표 달성해도 시간 내 계속 진행. 상태머신은 채점·표시용 유지.
**검증**: 백엔드 pytest 393 pass, 1분 회기 실측(알람→만료 다이얼로그→409), session-layout 8/8, layout-visual-gate 12/12.
- [x] **P2. 회기 계획·달성도 게이지** — 구현 완료(2026-07-14). 설계 1p `ops/session-progress-gauge-design-2026-07-14.md`.
단계별 누적 게이지(rapport_credit 파생, 회기 간 ×0.7 이월로 누적) + 상세 수치(방어/개방도/라포 누적+이번 회기 증가분)
를 세션 화면·상세 응답·스트림 done·대시보드 페르소나 카드(rapport_percent)에 노출. 계획서·프로토콜 첨부는 기존
`/personas/sources`(+신규 엑셀 업로드)로 흡수. **검증**: 2회기 carry 실측(65%→46% 시작 = ×0.7 정확).
- [~] **P3. 파일럿 인프라 대비** — 부하 검증 완료: `scripts/load-test-sessions.py`, 20 동시 세션 생성 20/20 성공
(p95 125ms, RAG warm 병목 없음), 5 동시 실턴은 p50 55s로 **claude_cli 상주 풀이 병목** → 파일럿 전
`ENGINE_MODE=claude_api` 전환 권고. 이관 절차 초안 `ops/server-migration-runbook-draft.md`, 파일럿 사용 지침
`ops/pilot-usage-guide-draft.md`. **남은 것**: 실제 서버 이관(연구팀·정보처 협의) — 외부 대기.
- [x] **P4. 데이터 업로드 흐름 검증** — 자유 양식 엑셀 업로드 경로가 없어서 신규 구축(2026-07-14):
`POST /personas/sources/upload`(xlsx/xlsm/csv → 텍스트 결정론 변환 `services/tabular_ingest.py` → 기존 마스킹·
hash-only 증거·sanitized chunk 경로 재사용, 원본 바이트 미저장) + PersonaStudio 파일 첨부 xlsx 지원.
**검증**: 파란 라벨 샘플 실업로드(NAME 마스킹·chunk 색인·임베딩 OK, raw hash-only 확인), 방어 테스트 9종
(.xls 거부·cp949 폴백·손상/빈 파일) pass.
- [~] **P5. 협업 체계** — 비개발자용 가이드 `guides/collaboration-for-researchers.md` 작성 완료(문구 수정 프로세스 포함).
**남은 것**: 저장소 초대(소유자 직접 실행).
- ⏸️ **외부 대기(구현 착수 금지)**: '실패(게임오버)' 정의 / 검수 표준화 항목 / AI 평가 노출 정책 / 페르소나 추가 제작 — 전부 연구팀 몫, 수신 시 반영.
## I. Outcome & Alliance OS — 정식 승격 로드맵 [에이전틱 구현]
> **2026-08-06 정식 승격.** 상세 계약·품질 목표·의존성은
> `ops/vignette-outcome-alliance-os-agentic-plan-2026-08-06.md`, 상태는 SSOT 대시보드의
> `Outcome & Alliance OS` G0~G8 계획 카드가 소유한다. 아래 작업은 사람 개발자 스프린트가 아니라
> 에이전트가 RED → 구현 → 통합 → 적대 검증 → runtime proof → SSOT 동기화로 닫는 증거 DAG다.
### I-0. G0 Measurement Truth Foundation
- [x] `app.measurement_event`, `app.measurement_instrument`, `audit.model_run` 계약과 provenance enum 구축.
- [x] 기존 `rapport_credit`, `alliance_level`, fast/deep 평가, Phase 3 KPI의 source map과 adapter 구축.
- [x] `simulated_state`·`model_inferred`·`agent_reported`·`learner_reported`·`human_rated`·
`observed_runtime` 층을 섞는 read model 실패 테스트 구축.
- [x] goal mismatch, task mismatch, empathic miss, withdrawal, confrontation, successful/failed repair를 포함한
첫 benchmark pack 구축.
- [x] **G0 종료 GATE 재검증** — post-merge dump의 disposable clone에 migrations 14/15/16을 각각 두 번
원자 재적용하고 current source census 29/29 producers, unknown/missing/null/orphan/호환성 위반 0을 확인했다.
증거: `ops/evidence/g0-provenance-census-scratch-post-g6-g8-20260807.json`.
### I-1. G1 Alliance Core
- [x] goal/task/bond의 내담자·관찰자·수련생 3관점 측정과 회기 전·중·후 pulse 구현.
- [x] 현재 `alliance_level`을 legacy simulation signal로 격리하고 새 alliance read model 구축.
- [x] 학습자 리뷰의 관점 차이·근거 장면 UI 구현.
- [x] 교수자 재평가를 append-only measurement로 구현.
- [x] **G1 종료 GATE** — 세 관점 독립 실행·저장, 축별 비가림 UI, 골든 장면 분류·근거 span,
평가 실패의 정상 점수 폴백 0건. **증거:** G1 focused backend 43 passed, PostgreSQL RLS/append-only/reveal
실원장 smoke, desktop/mobile Alliance Pulse 6/6 및 기존 회기 리뷰 12/12, prompt `1.2.0` 합성 gold 방향
정확도 9/9·최종 실패 0·근거 recall 88.9%. 실제 Session 진입 전 pre pulse를 fail-closed로 잠그고 첫 라운드
뒤 mid pulse를 비차단형으로 연결했으며, stable ID 재시도·변조 409·교차 회기 404를 통과했다. desktop/mobile
pre·mid focused E2E 2/2와 시각 증거 4장을 보존했다. 추가 source API/dev DB 실증은 한 회기의
`pre(degraded/insufficient_transcript) → mid(ready) → post(ready)`를 실제 turn UUID·ended_at과 함께 닫고,
동일 응답 안의 awaiting/external-event read skew와 degraded terminal DB 제약을 교정했다. 증거:
`ops/evidence/alliance-pulse-pre-mid-post-live-2026-08-07.json`. 현재 승격 기준 prompt 1.2+read-skew fix+
보수적 trailing-comma JSON 복구의 전체 재보정은 24/24 ready, 기대 방향 9/9, 오류·재시도 0,
evidence recall 94.4%·precision 73.9%를 기록했다. human-AI ICC는 내부 구현 종료가 아니라 Phase 3 외부 품질 목표다.
### I-2. G2 Longitudinal Outcome Trajectory
- [x] 최소 5회기 사례 arc와 예상 성과 궤적 분포 구현.
- [x] safety event와 outcome deterioration의 별도 계약·표시 구현.
- [x] 초기 1~5회기 off-track 탐지와 다음 확인 질문 구현.
- [x] case memory에 관계 사건·합의·미해결 균열을 role-safe하게 추가.
- [x] **G2 종료 GATE** — 5회기 기억·목표·관계 연속성, 악화 benchmark 조기경보 목표,
false alert·uncertainty 병기, 합성 궤적의 임상 결과 오표시 0건. **증거:** 4-case×5-session
benchmark status 정확도 1.0·false alert 0·critical miss 0, 실제 DB/API 5회기 연속 case와 15개 독립 축,
멱등 재시도·변조 409·학습자/코호트 RLS·role-safe 관계 기억 통과, desktop/mobile 체크인·궤적 12/12와
G1/기존 리뷰 15/15 회귀 통과. 추가 실제 PostgreSQL producer→store→evaluator smoke는 합성 교육용
악화 case의 `on_track→off_track→deteriorating`과 단발성 noise control의
`on_track→watch→on_track`·false alert 승격 0건을 실제 10회기·30 measurement·case별 5 revision으로
검증했고 learner/teacher 최신 projection이 일치했다. 원점수는 증거 JSON에 복제하지 않았고 임상 주장은 금지한다.
### I-3. G3 Rupture & Repair Lab
- [x] rupture/repair 상태머신과 이벤트 원장 구현.
- [x] Scenario Director의 자연스러운 균열 주입 구현.
- [x] 실시간 감지, 회기 후 장면 재생, 복구 분기 비교 구현.
- [x] 후속 발화에서 복구된 fast-loop 경고의 deep-loop reconciliation 구현.
- [x] **G3 종료 GATE** — 유형별 탐지·복구 benchmark, missed/partial/resolved E2E, 순응 유도 judge
gaming 0건, 문장 암기 전략의 변형 사례 실패 검증. **증거:** 11-case macro-F1 1.0·복구 상태
accuracy 1.0, critical miss/judge gaming/암기 false resolution 0, Scenario/G3 107 passed,
G0~G8 계약 251 passed, desktop/mobile 리뷰 50/50, 실제 API/DB durable-turn·멱등·RLS·교수자
append-only correction·최종 resolved 검증. 기존 수동 write smoke의 missed→partial→resolved와 별도로,
실제 `submit_turn`/공용 finalizer callback만 사용한 자동 runtime PostgreSQL smoke에서 3 episode·10 observation·
3 reconciliation, 세 상태, learner/teacher read projection, 세션 밖 evidence 참조 0건을 재현했다.
### I-4. G4 Deliberate Practice Engine
- [x] 평가 장면을 원자적 practice prescription으로 변환.
- [x] replay, branch, constrained response, voice retry, difficulty ladder 구현.
- [x] competency graph와 attempt evidence 저장.
- [x] 약한 역량과 망각 위험에 따른 다음 연습 자동 선택.
- [x] **G4 종료 GATE 재검증** — 모든 코칭 카드의 실행 가능한 재연습 연결, 전후 비교·근거 저장,
미지 사례 전 mastery 금지, 점수 보상 해킹 benchmark 통과. **증거:** G4 core/store/API 54 passed,
실제 API/DB durable-turn 처방→학습자 시도→교수자 append-only correction, 멱등 retry/변조 409,
learner/cohort RLS, reward-claim `practicing`·`mastery_allowed=false`, G4 UI 8/8 및 G1~G4 리뷰 58/58.
replay/branch/constrained response/voice retry/difficulty ladder 카드 CTA가 학습자 홈→사전 설정→실제 새 Session으로
이어지고 launch intent가 URL과 화면에 유지되는 desktop/mobile 22/22 및 통합 59/59를 통과했다. 추가 실제
PostgreSQL production caller smoke는 ready 평가·durable turn 2개에서 G4 submission/card/prescription/snapshot/
decision 각 1개를 자동 생성하고 재실행 시 같은 deterministic submission/card replay, episode·attempt·mastery·
transfer 승격 0건을 확인했다. 실제 API/DB/엔진 smoke는 prescriptions 2·episodes 6·attempts 6·
append-only correction 1·ready observer model-run 2를 통과했다. post-merge disposable clone의 브라우저는
route mock 0으로 desktop 2/2·mobile 2/2, G4 POST·read-model reload·before/after·멱등 retry를 확인했고
최종 episode/attempt/ready observer 각 1, UUID 노출·마이크 호출·overflow·중복 0이다.
증거: `ops/evidence/scratch-browser-returned-practice-closed-loop-evidence-20260807.json`.
- [x] **학생 자기주도 전체 루프 UI 계약** — 실제 `src` 학습 홈 추천→P1·목표 선택→텍스트/SSE 회기→
종료·리뷰→G4 공감적 반영 처방→키보드 CTA→새 회기 사전 설정·시작까지 desktop/mobile **2/2**,
typecheck와 고정 캡처 9장 시각 QA를 통과했다. 홈 CTA는 40px→44px 및 4.5:1 이상 대비로 보정했고,
390×844·320×568 활성 회기의 아바타/문구 겹침 0·축어록/입력창 카드 내부 포함과 리뷰→재연습의
내부 criterion/counterevidence/UUID/영문 이론키 비노출을 고정했다. 리뷰 1~5 척도는 320px에서
3+2 반응형 배열로 전환해 다섯 선택지 44px 이상·내부/페이지 overflow 0을 desktop/mobile 8/8로 고정했다. 이 검증은 모든
미소유 `/api/**`를 404로 닫은 로그인 없는 route fixture이며 실제 API/DB 폐루프라고 주장하지 않는다.
상세: `docs/ops/outcome-os-self-directed-learning-loop-ui-evidence-2026-08-07.md`.
### I-5. G5 Calibration Mirror & Transfer
- [x] 외부평가 공개 전 자기평가 잠금 흐름 구현.
- [x] 역량별 calibration error와 신뢰도 구간 계산.
- [x] 과신·과소신 맞춤 메타인지 연습 자동 처방.
- [x] 인구통계·관계스타일·난이도를 바꾼 transfer suite 구현.
- [x] **G5 종료 GATE 재검증** — 자기평가 오염 방지·수정 이력, 반복 연습의 calibration error 감소,
새 사례 역량 유지, subgroup 평가 드리프트 리포트. **증거:** 4-case expectation accuracy 1.0,
lock 뒤 오염 거부 1, calibration MAE 0.667→0.267, unseen 4/4 전이, 암기 false verification 0,
합성 subgroup drift 0.667 탐지. 실제 API/DB durable-turn revision→lock→독립 reveal·멱등/409·RLS,
G5 UI 4/4, G1~G5+기존 리뷰 62/62, typecheck/build 통과. transfer CTA는
`novelty=unseen_transfer`·suite·trial을 학습자 홈→사전 설정→실제 Session까지 보존하며 desktop/mobile
2/2와 실제 세션 통합 2/2를 통과했다. 추가 production caller 실 DB smoke는 prediction lock 전 observation
0건, lock callback 뒤 `failed`·`model_inferred`·`independent_observer` 관찰과 turn/session provenance
model-run 각 1개를 확인했다. 재실행 중복 0, passed·assessment·transfer suite/trial/승격 0이며
learner/teacher projection이 일치한다. 학생 후속 종료 회기에서 actual-execution POST·422 대기·503 재시도·
성공 reload·멱등 재확인 UI와 내부 UUID 비노출은 desktop/mobile 16/16으로 연결했다. 실제 API/DB/엔진
smoke는 execution·model-inferred·independent observer·instrument/evidence/model-run provenance 각 1과
멱등·오류 출처·미종료·타학습자·교차 cohort 차단을 통과했다. 같은 post-merge disposable clone 브라우저는
desktop/mobile 4/4로 G5 POST→reload→before/after와 1→1 멱등 재확인을 통과했고 UUID 노출·마이크 호출·
중복 원장 0이다. 합성 교육 fixture이고 임상·실제 숙달 주장은 금지한다.
### I-6. G6 Supervision & Research OS
- [x] attention queue와 learner drilldown을 새 측정 원장 기반으로 재구축.
- [x] 교수자 정정·AI 불일치를 calibration dataset으로 연결.
- [x] cohort curriculum gap, model drift, instrument version 비교 구현.
- [x] Phase 3 evidence manifest에 alliance/rupture/transfer/calibration 추가.
- [x] **G6 종료 GATE 재검증** — 위험·정체·미해결 관계 사건 3클릭 이내, 모든 집계의 원장 drilldown,
모델·척도 버전별 재현 리포트, public/teacher/learner 경계·감사 로그 검증. **증거:** 6-signal
queue 순서 정확·drilldown 1/상한3, metadata-only dataset raw transcript 0, version drift 탐지,
Phase3 네 도메인 전건. 실제 API/DB 멱등/409·AI-view·역할/cohort RLS·audit, version/subgroup/artifact
provenance hydrate 통과. 추가 scheduled producer는 G0~G5 실제 원장에서 attention/gap을 파생하고,
repo-approved synthetic baseline/candidate와 실제 measurement anchor 12개를 자동 비교해 batch 2·report 1·
subgroup 2를 저장했다. replay 중복 0, periodic 6 cohort 성공, drift/provenance hydrate, transcript 0·비임상
경계를 확인했다. G6 focused 37 passed, G6 UI desktop/mobile 6/6, typecheck/build 통과. 최신
safety-event metadata-only 우선 producer는 post-merge disposable clone의 current source에서 derive 201,
`safety_boundary(1) > deterioration(2)`, direct pointer 각 1, source detail canary 1·G6 projection canary 0,
raw transcript/임상 주장 0을 통과했다. NAS 승격은 배포 증거이지 internal 종료 조건과 분리한다. 상세:
`ops/outcome-os-g6-ledger-producer-evidence-2026-08-07.md`,
`ops/outcome-os-g6-version-comparison-producer-evidence-2026-08-07.md`,
`ops/evidence/g6-safety-priority-runtime-scratch-20260807.json`.
### I-7. G7 Multimodal Alliance
- [x] STT word timestamp, silence, overlap, interruption, prosody feature event 통합.
- [x] 텍스트·음성 평가 독립 산출 후 calibrated fusion 구현.
- [x] 장면별 오디오 재생과 음성 재연습 구현.
- [x] 비언어 특징의 과도한 임상 추론 차단 계약·테스트 구현.
- [x] Deepgram streaming STT adapter와 OpenAI batch fallback을 보존하고, 운영 기본 경로를 노트북 상주
`local_whisper`/`melotts`로 전환해 interim/final·word timestamp·provider event 계약을 동일하게 구현.
- [x] 저장 word token을 deployment `SESSION_SECRET` 기반 HMAC-SHA256 pseudonym으로 전환하고 원문 미보존을 고정.
- [x] 열린 streaming 중 동의를 1초마다 재검사해 철회 시 provider 전송·후속 저장을 중단하는 fail-closed 계약 구현.
- [x] 음성 단절/실패 뒤 작성 중 텍스트를 보존하고 텍스트 계속하기·키보드 가능한 음성 재연결 recovery UX 구현.
- [x] 외부 OpenAI TTS adapter와 AI 생성 음성 고지를 보존하면서, 운영 기본 TTS를 상업 사용 가능한
MeloTTS Korean(MIT) loopback 사이드카로 전환.
- [x] authenticated WSS `ready`에 STT/TTS provider·model을 노출하고, 마이크 열거·캡처 없는 public preflight가
네 기대값과의 정확 일치를 검사하도록 구현. full soak만 명시적 `--confirm-physical-capture` 뒤 장치를 연다.
- [x] synthetic gold target에 대해 text-only와 calibrated multimodal 출력을 직접 비교하는 1-MAE benchmark를
구현하고 `0.9533→0.9673`, 실측 gain `+0.014`(최소 `0.01`)와 gold 변조 반례를 통과했다.
- [x] consent mutation·timeline·measurement를 session 공통 PostgreSQL transaction lock으로 직렬화하고 실제
개발 DB 동시 철회에서 미커밋 중 writer 대기, 철회 커밋 뒤 저장 차단, timeline row `0`을 확인했다.
- [x] 10MiB audio buffer를 append 전에 거부하고 streaming event queue `32`, Deepgram receive queue `16`
고정했다. 단일 프로세스 cap probe는 10MiB high-water·초과 거부·clear 뒤 RSS 기준선 복귀를 확인했다.
- [x] 첫 음성 사용은 처리 목적·전송/전사·파생 신호·30일 보존·원음 미보존·철회/삭제·텍스트 대안을 설명하고,
서버 동의 원장 성공 전 `getUserMedia`와 voice WebSocket을 모두 0회로 유지한다. 원장 실패와 지연 권한 응답도
마이크 track/소켓을 남기지 않으며 숨은 Space 대신 안내 가능한 `Alt+M`을 사용한다.
- [x] process-local metadata-only `/admin/voice-runtime`과 Uvicorn `--ws-max-queue 4`를 추가해 worker RSS/CPU/FD,
WebSocket·provider session·audio buffer·event queue·overflow/fallback/error high-water를 관측 가능하게 만들었다.
- [x] public WSS evidence v4의 turn별 interim/speech-final/latency 계약, 같은 worker를 고정하는 runtime sampler,
exact image/container/cgroup/proc/Docker/TCP topology sampler, 독립 blind human-held-out voice-gain 계약과
네 artifact를 같은 public host·동시 시간창으로 묶는 `check-g7-external-proof.py`를 구현했다.
- [x] G7 내부 hardening exact SHA `a8c27b0aadcb9dacea86c4be5e62e19277b9cc2678ebca51aa02998d2c2d1a0d`
격리 NAS에 승격했다. clean candidate session E2E 24/24, OpenAPI 124, auth 401, G0~G8와 실제 NAS
browser SSE→DB review를 통과했고 물리 마이크는 사용하지 않았다.
- [ ] **G7 종료 GATE** — 오디오·자막·측정 이벤트 시간 정렬, 물리 마이크·공개 WSS 장시간 E2E,
텍스트 단독 대비 음성 이득 benchmark, 음성 보존·삭제·동의 정책 검증. **내부 완료:** 실제 API/DB
동의→단일 시계→독립 측정/융합→철회·삭제, actual-Postgres 동시 철회, synthetic gold gain benchmark,
bounded queue/10MiB cap RSS probe, Deepgram adapter, HMAC word pseudonym, 1초 동의 재검사, 선행 동의 원장,
recovery UX, mic-free expected-provider/model preflight와 외부 증거용 네 artifact checker를 확인했다. 최신
격리 synthetic PCM soak는 59/59 연결 종료·성공 발화 55·철회 차단 2·미동의 차단 1·overflow 거부/복구 1/1,
focused 119 passed를 통과했다. 공개 G7 route는 TLS 1.3 handshake·비인증 1008·Cloudflare edge와 마이크
열거/캡처 0을 확인했다. 증거: `ops/evidence/g7-public-wss-preflight-current-20260807.json`.
**남음:** 사용자가 장치를 선택하고 명시 동의한 물리 마이크 3,120초, 공통 3,000초 양방향 soak와 동시
runtime/topology high-water, 독립 라벨 held-out/pilot의 실제 음성 이득,
실제 worker·Uvicorn·OS·Cloudflare edge를 같은 50분 시간창으로 묶은 RSS/CPU/queue high-water.
preflight·synthetic·옵션 제공만으로 종료하지 않는다.
### I-8. G8 Autonomous Content & Continuous Improvement
- [x] source pack 기반 새 사례·균열·연습·benchmark 초안 자동 생성.
- [x] 안전·정체성·난이도·정답 누수·문화적 편향 독립 적대 검토.
- [x] 승인된 콘텐츠만 catalog 승격하고 provenance 보존.
- [x] metadata-only 운영 incident를 승인 source로 변환해 새 adversarial benchmark pipeline을 실행.
- [x] repo-approved synthetic source를 durable lease/retry job으로 등록하고 API lifespan에서 opt-in 주기 실행.
- [x] 운영 측정의 약한 benchmark·drift를 주기적으로 탐색해 위 incident→adversarial 경로를 자동 기동.
- [x] 실제 Claude pending payload를 사람 검수하고 승인된 projection만 catalog에 0→1로 소비하는
관리자 browser/API/DB E2E. learner/teacher 403·replay 멱등·변조 409·민감 payload 비노출.
- [x] release agent의 결정적 patch·manifest·clean candidate·API/Web/회기 gate, 격리 NAS 승격,
semantic runtime/browser proof, 이미지 rollback, exact-SHA SSOT handoff를 fail-closed로 구현·독립 감사.
- [x] 위 release agent로 변경 SHA `e9b482ccba616a610c1f4862366d44aabae20ee8c82d8ef7f2cf16069f2d6e99`
실제 격리 NAS에 승격하고 postdeploy browser·DB review·rollback snapshot·exact-SHA SSOT handoff를 남겼다.
- [x] **G8 내부 구현·실제 rollback receipt GATE** — source→draft→red-team→benchmark→catalog 재현, 정답 누수·PII·무근거
임상 주장 0건, 모델 교체 자동 calibration/rollback, 운영 오류의 재현 테스트·backlog DAG 자동 환류.
**현재 코드 증거:** caller가 완성 JSON을 넣는 기존 경계와 별도로 source hash·PII fail-closed→structured
generator→서로 결과를 공유하지 않는 red-team 2개→variant generator→독립 judge 3개 이상→durable
`draft_payload`·`pending_human_approval` 저장 worker/API를 추가했다. 모델/structured output 실패, PII,
answer leakage, 무근거 claim, reward hacking은 store 호출 전 차단하고 같은 submission은 model call 0으로
재생한다. repo-approved source의 durable lease/retry scheduler를 lifespan에 opt-in 연결했고 실제
`claude_cli/claude-opus-4-8` 7 calls→dev PostgreSQL reviewer 2·variant 3·pass rate 1.0→
`pending_human_approval`과 동일 UUID 0-call replay를 확인했다. full API/gateway 858 passed,
Ruff·OpenAPI type check·web typecheck 통과. G6 canonical 임계값·subgroup 원장을 재검증하는 default-off
drift trigger는 실 DB incident 1·DAG 4·job 1·approval/catalog 0과 동시성·멱등을 통과했다. 기존 실제
Claude pending payload는 새 모델 호출 0회로 관리자 browser에서 catalog 0→1·approval 1, 역할 403·
replay 멱등·변조 409를 통과했다. release agent는 독립 감사 4건 수정 뒤 14/14와 전체 API 891 passed를
통과했다. exact SHA의 NAS runtime은 health·OpenAPI 123·auth 401·G0~G8와 browser SSE→DB review를
통과했고 transient 502 rollback/recovery와 최종 readiness 3회째 성공을 기록했다. 상세:
`ops/outcome-os-g8-agentic-worker-evidence-2026-08-07.md`. 현재 소스의 HTTP control-plane executor는
artifact SHA·idempotency key·receipt/evidence가 일치할 때만 `executed`를 허용하고 미구성은 `requested`,
호출/receipt 실패는 `failed`로 남긴다.
**2026-08-07 종료:** 격리 NAS `vignette-preview-20260807`에서 실제 receipt-bound image rollback을 두 번
실행했다. 별도 executor helper(`127.0.0.1:18149`, host network·read-only rootfs·cap-drop ALL·
no-new-privileges·uid1028/gid100·group101)와 별도 lifecycle의 control-plane API(`127.0.0.1:8018`,
producer disabled, preview DB `172.22.0.2``vignette_app` role)를 띄우고, `rollback-old` plan은 receipt
`nas-g8-723eeef22eab05e63e3fafb0`으로 previous 이미지 `79ec…4450`/`c530…2f28`를, `restore-current` plan은
receipt `nas-g8-2738846cf2cf4fbe8ce0fc26`으로 current 이미지 `52e0…8b2d`/`6fdb…f215`를 활성화했다.
release gate·approval 각각 2회 멱등, `audit.ci_lifecycle_event` `rollback/executed` 2건과 receipt id 결속,
`audit.ci_human_approval_event` `authorize_rollback` 2건, silent auto-promotion 0, HMAC journal 6-record
체인 검증, 매 실행 뒤 health 3/3과 최종 OpenAPI 126·auth 401·Web 200을 확인했다. helper 격리는 image가
상속하는 `com.docker.compose.*` label을 baseline으로 두고 project/service만 격리 override하는 계약으로
검증했다(`scripts/launch-nas-preview-g8-helpers.py`, fake-runner 37/37). 계획했던 Windows SSH 터널은 NAS
sshd가 `administratively prohibited`로 거부해 sshd 설정 변경 대신 같은 격리 계약의 NAS-side probe
컨테이너로 실행했고 control-plane/preview origin 분리는 유지했다. `down`/`volume rm`/`prune` 미실행,
공개 런타임 미접촉, 비밀 env 파기, helper 0·listener 0 복귀. 상세:
[런북](./ops/nas-preview-g8-rollback-proof-runbook.md) ·
[기계 판독 증거](./ops/evidence/nas-preview-g8-actual-rollback-2026-08-07.json).
- [x] 2026-08-07 release-only SHA `6030a677…c611`을 격리 NAS에 승격했다. patch 3회 결정성,
localhost candidate browser E2E 108/108, postdeploy SSE→DB review 32.437s, health 3/3,
OpenAPI 126·auth 401·G0~G8 route, named DB volume 보존을 확인했다. 이는 당시 runtime promotion
증거이며 현재 HEAD 배포 증거가 아니다.
- [x] **clean-head release mode** — 명시적 `--source-mode clean-head`와 별도 detached source root를 추가했다.
tracked-dirty 차단, exact HEAD/tree/archive SHA, 2회 결정성, archive-only candidate, patch-mode 무회귀,
증거 binding, source-only insecure-context 12/12 gate(UUID 6 + SHA-256 6), commit 전 NAS-origin 11-spec rollback gate를 unit
30/30·Ruff·py_compile로 고정했다. 첫 execute는 archive CRLF를 candidate에서 정규화하도록 fail-closed했고,
두 번째 execute는 API/types/build 뒤 109/5에서 fail-closed해 둘 다 NAS mutation 0이다.
- [x] **G8 NAS-origin 재검증**`randomUuid()`와 portable SHA-256 수정, insecure-context 12/12 회귀를 포함한 current HEAD를
위 clean-head mode로 격리 NAS preview에 배포한다. `started_at`의 9시간 오차 P0는 UTC `+00:00`, KST 달력 날짜,
KST browser focused 실DB E2E로 수정했고 API 전체 921 passed와 코칭 이력 1/1을 통과했다. 시간종료 모달 강제
닫기 우회는 없다. 첫 clean commit `5221f79e` execute는 API/types/build와 candidate DB-backed 회기 후반까지
통과했지만 Vite 전용 `/src/main.tsx` 차단 테스트가 production hashed entry를 차단하지 못해 110/2에서
mutation 없이 중단됐다. source와 production preview에서 수정 회귀 2/2씩 통과했다. 이 테스트 수정의 새 clean
commit `21461ab3` candidate 112/112 뒤 실제 NAS-origin G4 2건이 `crypto.subtle` 부재로 실패했고, release agent는
이전 image·health·auth·OpenAPI를 검증해 rollback했다. 문장 원문을 보내지 않는 portable SHA-256 fallback은
secure localhost 2/2와 Tailnet insecure origin 2/2에서 동일 표준 digest를 통과했다. 최종 clean source HEAD
`61a41d1f…6af`·tree `87dec55d…3b77`·2회 동일 archive `4d15d055…119d4d`를 NAS에 승격했고 candidate
112/112와 실제 `http://100.116.83.60:8088` origin browser review 112/112(11 specs, desktop/mobile/single-run),
health ok·db/engine true·auth 401·OpenAPI 126을 통과했다. API/Web는 `d5021950…e4b1`/`9796c092…4b36`, fresh dump는
`36ec8748…24db8` 1,097,100 bytes·TOC 1,738/TABLE DATA 129이며 previous `6030a677…c611`을 보존했다.
### I-DAG. 첫 에이전트 실행팩 AOS-001~012
- [x] **AOS-001** 기존 지표 provenance inventory — 선행 없음. 종료: 모든 현재 점수의 source map.
- [x] **AOS-002** `measurement_event` ADR/schema/DDL — 선행 AOS-001. 종료: contract + rollback + RLS tests.
- [x] **AOS-003** benchmark case schema와 첫 8개 장면 — 선행 AOS-001. 종료: deterministic fixtures.
- [x] **AOS-004** legacy rapport/alliance adapter — 선행 AOS-002. 종료: 기존 데이터 무손실 read model.
- [x] **AOS-005** goal/task/bond 3관점 structured schema — 선행 AOS-002/003. 종료: gold contract tests.
- [x] **AOS-006** session measurement API — 선행 AOS-005. 종료: API/DB idempotency E2E.
- [x] **AOS-007** 학습자 self-assessment lock — 선행 AOS-002. 종료: reveal-order E2E.
- [x] **AOS-008** Alliance Pulse 리뷰 UI — 선행 AOS-006/007. 종료: desktop/mobile visual proof.
- [x] **AOS-009** 교수자 measurement drilldown — 선행 AOS-006. 종료: role/RLS/audit E2E.
- [x] **AOS-010** measurement drift/calibration job — 선행 AOS-003/005. 종료: version comparison report.
- [x] **AOS-011** public/deploy proof manifest — 공개 API 126 paths, G1~G8 route, Pages asset, auth/OAuth proof.
- [x] **AOS-012** SSOT/TODO 동기화 — dashboard/TODO/backlog/release manifest 동기화와 checker green.
---
## 지금 임계경로 (한 줄)
납품(공개 데모)의 핵심 블로커는 **A(공개 `/turn` proof + 배포 secrets)**. 그 다음이 **B(한신대 거버넌스)**와
**C(Phase 3 파일럿)** — C는 2026-07-13 회의로 **7월 말~8월 초 파일럿 일정 확정**, 그 전제로 **H의 P1(회기 종료 시간 기반)**이
새 구현 게이트다. **I(Outcome & Alliance OS)는 현재 감사 기준 G0~G6과 G8이 DONE이다.
G8은 receipt-bound 실제 rollback과 current clean source NAS-origin 112/112를 완료했다. G7은 내부 구현 DONE과 외부 proof GATE를 분리하며,
current source 공개 배포·local voice ready·authenticated WSS 무마이크 rehearsal은 완료했고, 명시 동의 mic 3,120초·동시 resource high-water·독립 라벨 benchmark·checker exit 0이 남았다.** D~G는
품질·확장 작업으로 게이트가 아니다. 상세 상태·검증 증거는 SSOT 대시보드가 소유한다.
- [ ] **직렬 E2E 게이트 잔여(코칭 기능 통합)** — 소유자의 70d7cb34(AI 코칭 버튼·버블)가 세션 UI에 새 DOM을 더하며
session-mvp strict 위반('코칭' 버튼 3개)·동맹 체크포인트 포인터 타임아웃(결정적 재현)이 발생. 통합 마감 후
교사 재평가 2건 포함 전체 GREEN 재검증. settings/learner 협폭 레이아웃 결함 91건(sweep 보고)도 함께.
상세: docs/ops/evidence/usecase-tdd-2026-08-18.json

View file

@ -0,0 +1,244 @@
# Vignette 백로그 — 열린 항목만 (현행본)
> **SSOT는 `docs/dev_dashboard.html`이다.** 상태·검증 증거·결정·로드맵의 권위 기준은 대시보드다.
> 이 파일은 대시보드와 일치하는 **아직 열린 작업(외부/환경 게이트)** 만 얇게 유지한다.
> 완료된 작업의 변경 로그·검증 기록(구 "최신 동기화 추가"·"이번 세션에 닫은 것"·B1~B3 `[x]` 상세)은
> `docs/archive/ops/backlog-2026-06-26-full-history.md`로 냉동 보관했다. 필요할 때만 이력용으로 본다.
>
> 운영 원칙: 가짜 증거로 DONE 표기 금지. 실증 불가·외부 의존·소유자 결정 항목만 여기서 추적한다.
> 분류: **B1 비차단 폴리시** · **B2 환경 제약(증거 생산 불가)** · **B3 소유자 결정** · **B4 외부 거버넌스**
> **B0. 원천문서 갭** — 한신대 산학협력 원천문서 갭(critical/high/medium)은 SSOT 대시보드 "원천문서 갭 분석"
> 섹션과 상세 근거 `docs/ops/source-docs-gap-analysis-2026-06-26.md`에서 추적한다. C1~C3/H·M 구조는 코드로
> 선제 구축했고, 임상 문안·평가기준·골든셋 콘텐츠는 임상팀(구훈정·어유경) 외부 정의로 받는다.
>
> **개선관리 워크북(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` 전에는 완료로 닫지 않는다.
> 현재 개선관리 집계는 **완료 9·검토 2·총 11**이다. C-001 외부 입력 46칸은 조작하지 않는다.
>
> **2026-08-29 최신 운영 상태(이 PC 로컬 기준)·2026-08-31 재실측 정정** — 당시 이 PC 로컬 API 8001은 연결 거부였고 이 PC의
> Docker daemon 부재로 이 PC 기준 DB/API는 복구 불가였다(이 PC는 개발 워크스테이션). <b>2026-08-31 재실측:</b> public 도메인
> `https://api-vignette.chanpaca.net/health`는 별도 호스트(Cloudflare 뒤)에서 200·`environment=prod`·`db=true`·`engine=true`
> 정상 서빙 중이며, 이 PC에는 cloudflared 프로세스가 없어 public 도메인은 이 PC가 아니다. 즉 2026-08-29의 OUTAGE 표현은
> 도메인 장애가 아니라 이 PC 로컬 상태였으며 오늘 실측이 이를 대체한다. 배포된 동적 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과
> AOS-001~012는 `docs/TODO.md` I절에서 전건 추적하고, 상태는 SSOT 대시보드의 9개 계획 카드가 소유한다.
> 이 얇은 백로그에는 그중 외부·환경 증거가 필요한 항목만 기존 B2/B4/Phase 3 게이트와 합쳐 유지한다.
>
> **2026-08-31 재검증(목표 마무리 기록)** — 아래 열린 항목 전건을 SSOT 대시보드(2026-08-29/30)·최신 핸드오프(`ops/handoff-goal-production-2026-08-29.md` §0)·실제 환경과 교차 재확인했다. 실증 게이트가 남아 있어 **가짜 증거로 DONE 체크하지 않는다**(운영 원칙).
> **운영 인프라 정정(중요):** 정식(production)은 `infra/docker-compose.nas.yml`**NAS `vignette-prod` 스택**(`ssot-host: nas`·`runtime-class: production`)으로 분리 운영 중이다. 2026-08-31 실측으로 public 도메인 `https://api-vignette.chanpaca.net/health`가 200·`status=ok`·`db=true`·`engine=true`·`environment=prod`·`engine_mode=openai`로 응답하는 반면, **이 Windows PC에는 cloudflared 프로세스가 없고 로컬 API `8001` listener도 없다** — 즉 public 도메인은 이 PC를 지나지 않는다. 이 PC는 개발 워크스테이션(과거 자택 public 호스트였던 경로의 잔여 상태: Docker `vignette-dev-db:55432` Up·로컬 엔진 `9099` LISTENING·`VignettePublicRuntime`/`Watchdog` 2개 Scheduled Task Disabled)이다. 따라서 아래 항목 중 이 PC의 `8001`/task/재부팅 관련 게이트(B2)는 **정식(NAS)과 분리된 이 PC/개발 런타임 기준**으로 읽어야 하며, public 도메인 장애를 뜻하지 않는다. push·Pages 배포·public runtime/task 변경은 별도 승인 전 미실행.
> 실제 닫힘 판정은 이 PC 기준이 아니라 **정식 운영 주체(NAS `vignette-prod` ingress·DB·도메인) 기준**으로 해야 하며, 이 PC에서 NAS ingress를 직접 실측할 수 없어 별도 확인이 필요하다. 2026-08-31 추가로 **public 도메인 읽기 전용 실브라우저 폐루프**(Playwright chromium: 로그인 화면 1 passed·데스크톱/모바일 200·Google OAuth redirect_uri 확인·dev-login 차단·`/auth/me` 401 외 에러 0)를 실증했다(증거 <code>ops/evidence/public-domain-readonly-browser-2026-08-31.json</code>) — 이는 public 도메인 정상 서빙의 추가 증거이며, 실제 Google 계정 로그인→callback→관리자/P20 실회기·턴 생성(production DB mutation)은 소유자 승인·계정 세션이 필요해 여전히 열린 게이트다. `public API/DB·아바타 정식 승격`, `G8 실DB/public 사람 게이트`, `음성 캐스케이드 live`, `DB 백업 운영화`, `vnet.18ka.net live`, `claude_cli↔Anthropic API live`, `Compose infra/.env`, `B1 티켓 자동 분류`, `C-001`, `한신대 거버넌스`, `L1 등재`, `Phase 3`은 전부 열린 게이트로 유지한다. 참고: `공개 DB 계정·회기 복구 안정화``stable-source 재부팅 후 watchdog smoke`는 사실상 **같은 잔여 게이트(실제 Windows 재부팅 후 자동복구 smoke)**를 추적 중이다 — 재부팅 smoke가 닫히면 두 항목과 REQ-008이 함께 닫힌다.
---
## B1. 비차단 코스메틱/후속 폴리시
- [ ] **운영 티켓 자동 분류·처리 후속** — Claude Recipe headless 자동 수정 후보, 관리자 승인 후 이슈 등록·PR/작업
스레드 생성, 처리 결과 audit trail 확장은 아직 설계/승인 필요. 담당 그룹 자동 배정·우선순위 escalation·raw/rollup
보존기간 같은 운영 정책은 B3에서 이미 결정됨(수동 승인·미도입 고정). 자동 수정은 운영자 승인 전까지 실행하지 않는다.
> B1 완료 항목(학생 리뷰 1~5 척도 320px 반응형 재배치, 셸 구분선, 세션 종료 UX/다크테마, 아바타 SVG 리그 복귀와 래스터 비활성화, SEO/공유 카드, 레이아웃 정렬, 권한 위임,
> 메일링, 아카이브 API, TTS voice map, 빈상태 레이아웃, 평가 실패 복구 UX, SSE 저장, live-coach 표면화,
> 음성 메타, 위기 게이트 UI, 운영 티켓/헬스 샘플러 등)의 상세·검증 로그는 아카이브 full-history 참조.
---
## 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가 정상이다. 기존 인증 세션은 복사하지
않아 사용자는 Google 재로그인이 필요하다. current dump·old-original dump·전환 직전 rollback container를
모두 보존하며, 실제 사용자의 소유 회기 확인과 자동 백업 운영화 전에는 제거하지 않는다.
2026-08-09 Docker Desktop 중단과 18:02:55 KST Docker Desktop UI의 active DB 직접 stop으로 `db=false`
재발했지만 동일 recovered container/volume만 재기동했다. 현재 owner read-only 집계 users 356/sessions 193/
turns 726, Google users 16/Google-owned sessions 31이고 public db/engine true다. 2026-08-09 fresh dump
`vignette-dev-db-vignette-20260809-122235Z.dump` SHA `f1fd569c…5f64`를 보존했다. 공개 API/cloudflared는
detached-clean `a73bcd24…`·tree `b02b3a1b…`로 승격했고 OpenAPI 126·local voice exact·receipt
`47388d58…9b08` passed를 확인했다. watchdog·로그온 boot도 같은 stable root의 commit/tree/script SHA에 pin해
명시 실행 결과 0·Ready를 확인했다. 실제 Windows 재부팅 후 자동복구 smoke는 남아 있다.
2026-08-12에는 휴면 Gradle daemon의 `8001` 점유·cloudflared/voice sidecar 중단과 PowerShell 5.1 native stderr
조기 종료 결함을 복구했다. fresh dump `9cbda31e…0ce5`(7,532,625 bytes·TOC 1,492)를 먼저 고정하고
detached-clean `a73bcd24…`·tree `b02b3a1b…`로 fresh 승격해 receipt `47388d58…9b08`, public health 3/3,
OpenAPI 126·local voice exact와 watchdog/boot result 0을 재확인했다.
2026-08-29에는 Windows PowerShell 5.1의 timed `WaitForExit()`가 process handle 선확보 없이는 성공한 web
build의 `ExitCode`를 null로 남기는 오판을 제거했다. 실제 PS5.1 계약 32 passed, detached-clean
`44b7835c…`·tree `06133249…`에 boot/watchdog을 동일 핀으로 재등록했고, 5174 자동복구·HTTP 200,
`LastTaskResult=0`·failcount 0, local/public health `status=ok·db=true·engine=true`를 확인했다. 실제 Windows
재부팅 smoke와 PC 비종속 상시 호스트 이전은 계속 열린 환경 게이트다.
상세: `docs/ops/public-db-recovery-rehearsal-2026-08-07.md`.
- [ ] **DB 백업 운영화** — 검증형 one-shot custom dump와 실제 일회용 restore drill은 통과했다. 예약 실행,
실패 알림, NAS/off-host 암호화 복제, restore drill 주기와 보존 정책을 운영 게이트로 남긴다.
- [ ] **`vnet.18ka.net` 공개 live 전환** — 코드/스크립트는 `vnet.18ka.net`·`api-vnet.18ka.net` 허용까지 갱신됨.
남은 블로커: (1) `vnet.18ka.net`/`api-vnet.18ka.net` DNS A record 부재, (2) cloudflared 현재 인증 컨텍스트가
`18ka.net` zone이 아니라 `chanpaca.net` zone에 route dns 생성, (3) Google Console redirect URI
`https://api-vnet.18ka.net/auth/callback` 등록 필요. 현재 prod API 8001은 `db:true`/`engine:true`로 건강.
상세: `docs/ops/tailscale-vnet-runtime-2026-06-27.md`.
- [ ] **Compose 패키징용 `infra/.env` owner-secret fill-in** — 현재 production direct-runtime은 서로 다른 G3~G8
내부 토큰 6개, 필수 운영키, DB `current_user=vignette` non-owner app-role preflight와 공개 health를 통과했다.
향후 Compose 패키징 전환 시 `APP_DB_PASSWORD` 등 패키징 비밀과 prod-safe engine/voice flags를 별도 주입하고
compose profile preflight를 다시 닫는다.
- [x] **공개 Google OAuth 실제 `/turn` proof** — 2026-08-28 기존 Google 세션 callback→관리자 진입과 공개 P20
회기 생성→자기점검 잠금→실제 학습자/AI 내담자 턴→종료→리뷰·평가→다음 회기 버튼 재노출을 브라우저로
확인했다. 신규 Gmail 선택은 별도의 Vignette 계정 생성 행동시점 확인 항목이다.
- [ ] **음성 캐스케이드 live** — 공개 G7 API route와 TLS 1.3 WSS handshake, 비인증 1008 차단, 로컬
synthetic short soak는 통과했다. Deepgram/OpenAI fallback adapter와 운영 기본 `local_whisper`/`melotts`의 interim/final/word timestamp, HMAC word pseudonym,
streaming 중 1초 동의 재검사, 공통 consent transaction lock, bounded queue/10MiB cap, 텍스트 보존·음성 재연결
UX, `ready` provider/model 계약은 code/internal 완료다. actual-Postgres 동시 철회는 writer 대기→커밋 뒤 차단과
timeline row 0, synthetic gold benchmark는 text-only 1-MAE `0.9533`→multimodal `0.9673`을 확인했다.
public runner의 preflight는 auth cookie·소유 회기로 WSS ready/ping과 기대 STT/TTS provider/model 네 값을
마이크 열거·캡처 없이 검사한다. full soak는 `--confirm-physical-capture` 없이는 장치를 열기 전에 exit 3이다.
Session 첫 음성 사용은 30일·원음 미보존 서버 동의 원장을 먼저 기록하며, 성공 전 `getUserMedia`와 voice
WebSocket은 0회다. public runner v4의 turn별 interim/speech-final/latency, 관리자 single-worker runtime
high-water, exact image/container/cgroup/proc/Docker/TCP topology sampler, held-out 30명 외 calibration split
참가자를 포함한 총 최소 31명/held-out 50회기/150축 독립 blind human-held-out voice-gain evaluator와 이를
같은 public host·동시 50분 시간창으로 묶는 canonical checker까지
code/internal 준비를 마쳤다.
독립 평가 입력은 `prepare-g7-human-voice-gain-intake.py --create-template`의 명시적 non-evidence 빈 틀로 시작하고,
`--compile ... --out ...`가 비식별 CSV의 production floor·ICC·κ·gain·CI를 재계산해 모두 통과할 때만 pack을
생성한다. 이 작성 도구는 준비됐지만 실제 참가자·독립 평가자 데이터는 아직 수집되지 않았다.
운영 TTS는 MeloTTS Korean(MIT) loopback으로 전환했고 외부 OpenAI adapter와 회기 전·중 AI 생성 음성 고지는
보존했다. clean public runtime·`local_whisper`/`melotts` exact ready·authenticated WSS의 30초 무마이크 rehearsal은
voice/runtime/topology 각 7 samples로 통과했다. 남은 범위: 실행 직전 장치 선택·명시 동의 물리 마이크와
3,120초 양방향 soak, 독립 라벨 held-out/pilot voice-gain benchmark,
같은 실행의 실제 worker·Uvicorn·OS·Cloudflare edge RSS/CPU/queue high-water. 네 artifact가
`scripts/check-g7-external-proof.py` exit 0을 만들기 전에는 닫지 않는다.
current source의 `start-public-runtime.ps1`는 sidecar-before-API lifecycle, exact
`local_whisper/small/cpu-int8`·`melotts/melotts-korean` readiness, provider env, Uvicorn
`--ws websockets --ws-max-queue 4`를 fail-closed로 연결했다(launcher/sidecar 80/80, API voice 71/71).
공개 topology용 Windows host mode도 PID/start/exe·command SHA/cwd, RSS/CPU/handles/threads,
listener owner/conflict와 TCP high-water를 검증한다. 공개 runtime은 detached-clean `a73bcd24…`에서 receipt-bound로
실행 중이고 task source pin도 result 0이다. 물리 마이크 창은 사용자 장치 선택·명시 동의 전에는 열지 않는다.
코드 P0는 완료됐다. runner exit는 canonical checker `exit 0`/`gate_closed=true`에 결속되고, browser Origin은
API/WSS/admin/topology host·scheme과 분리 검증되며, 실제 capture 최소 3,120초와 세 artifact 공통 overlap
`≥3000s`를 강제한다. Windows topology는 detached-clean HEAD/tree, runner/collector/checker SHA와 exact
`psutil==6.1.1`까지 pin한다. fresh launcher는 legacy API/exact-config cloudflared를 bounded 교체해 새
PID/start/exe/command SHA/cwd safe receipt를 만든다. current runtime/checker 6파일 94/94, intake 6/6,
standalone validator 4/4를 합친 104/104를 통과했다.
무마이크 rehearsal은 완료됐지만 물리 마이크·human pack을 대체하지 않는다.
격리 NAS 프리뷰 `http://100.116.83.60:8088`은 전용 Compose 프로젝트·포트·네트워크·볼륨에 배포했고, 실제 브라우저 회기와
review API 저장 축어록 2턴을 확인했다. 기존 프로젝트 중단·재생성 명령은 실행하지 않았다. 매시간 `Vignette 앱 상태·회기 E2E 정기 검증`
heartbeat(automation id `vignette-e2e`)는 ACTIVE이며 공개/NAS 화면·health는 읽기 전용으로 보여준다. NAS 112건은 실 API/DB 22와 route fixture 90으로 영수증에서 분리한다. 동일 학습자 SSE→review→G4/G5 actual 폐루프 runner의 clean HEAD/tree·보호 포트 거부·sentinel·exact cleanup과 unit 14/14는 완료됐다. 최초 세 full run은 하네스, 4차는 reload ledger hydration, 5차는 fixture 공유, 6차는 mobile success-state locator를 fail-closed했고 7차 clean `aa81af29…`가 첫 전체 GREEN을 만들었다. `94666192…``f97e7fad…`의 DB init 실패는 두 번째 receipt 로그로 detached `99_app_role.sh` CRLF와 `bash\r` exit 127이 원인임을 확정했다. `.gitattributes``*.sh text eol=lf`로 이를 닫았다. 공통 톱바 desktop/mobile 44px을 포함한 clean `b02bee26…`·tree `8f677bba…`의 최신 single full run은 487.3초 GREEN이다. receipt `periodic-learner-e2e-20260812-171547.json`·SHA-256 `8c11a136…af8e3`은 same-learner SSE·review·G4/G5 `0→1→1`, returned desktop/mobile 4/4, route mock 0, public/NAS/active DB 접촉 0과 cleanup 0을 증명한다. heartbeat는 이 GREEN이 6시간 이상 오래됐거나 material milestone이 바뀔 때만 다시 실행한다. release gate·NAS preflight·배포 SHA 변경 때만 프리뷰를 갱신한다. 2026-08-07 SHA `6030a677af7e87cbfabc422b553d108d53414fd3c446548734a13b036d35c611`의 localhost 108/108과 평문 origin UUID 24건 실패는 역사 기준선으로 보존한다. 실제 receipt-bound rollback은 같은 프리뷰에서 별도 helper로 두 번 실행해 종료했다([런북](./nas-preview-g8-rollback-proof-runbook.md), [기계 판독 증거](./evidence/nas-preview-g8-actual-rollback-2026-08-07.json)). 현재는 source HEAD/tree `61a41d1f…6af`/`87dec55d…3b77`의 active archive `4d15d055…119d4d`, exact API/Web `d5021950…e4b1`/`9796c092…4b36`이 실행 중이다. candidate 112/112와 실제 NAS-origin 112/112, health ok·db/engine true·auth 401·OpenAPI 126을 통과했고 fresh dump `36ec8748…24db8` 1,097,100 bytes·TOC 1,738/TABLE DATA 129와 previous `6030a677…c611`을 보존했다. 증거: [배포 증거](./nas-preview-deployment-evidence-2026-08-07.md),
[브라우저 증거](./evidence/nas-preview-live-turn-2026-08-07.png). 위 health GREEN은 배포 당시 증거다.
2026-08-12 current NAS는 고정 LAN engine relay listener 상실로 degraded였고 15:24 KST local health 요청도
3회 타임아웃했다. source-pinned relay launcher와 loopback contract 8/8에 더해 detached-clean
`f08e03de…f191`·tree `00275133…0abd` exact check-only도 `mutation=false`·listener `0→0`·runtime state 0으로
PASS했다. 실제 9100 시작·NAS origin 재검증은 소유자 승인 대기다. 명시 동의 물리 마이크와 독립 human voice-gain 증거도 아직 없으므로
G7은 external GATE로 유지한다.
- [ ] **claude_cli ↔ Anthropic API live 동일성** — provider 라우팅·Anthropic `/v1/models` 탐색·지원 추론 강도·관리자 fail-closed 저장 경로는 구현 완료. 남은 범위는 연구팀/기관 `ANTHROPIC_API_KEY`를 게이트웨이 호스트에 주입한 live 응답·계량·오류 표면화 비교다. Claude CLI·Codex CLI(Terra/Medium)·Agy CLI(Gemini 3.6 Flash/High)는 로컬 live probe를 통과했다.
- [ ] **stable-source 재부팅 후 watchdog smoke** — 2026-08-27 Docker Desktop 4.82.0이 stale
`dockerInference`/`engine.sock` AF_UNIX reparse socket(ERROR 1920)에서 crash-loop한 사건은 두 부모 IPC
디렉터리를 `.stale` 백업명으로 이동한 뒤 정상 복구했다. container·image·volume은 생성/삭제/교체하지 않았고,
기존 `vignette-dev-db`와 recovered volume만 재기동했다. 2026-08-28 watchdog·로그온 boot는 detached-clean
`99779a6a…fbc8`·tree `57da86aa…6b91`와 exact commit/tree/script SHA에 재등록했다. 두 작업의 60분 상한·
`IgnoreNew`, launcher mutex·bounded readiness/build·process-tree 종료, hard-down 즉시복구를 focused 50/50으로
검증했고 watchdog 18:10:57 KST·boot 18:14:27 KST 수동 실행 `LastTaskResult=0`·failcount 0을 확인했다.
public P20 authenticated `/turn` 폐루프도 완료됐다. 남은 것은 실제 Windows 재부팅 후 엔진/API/터널 복구다.
DNS 개통 후 `api-vnet.18ka.net``-AdditionalPublicHealthUrls`로 명시 추가한다.
상세: `docs/ops/public-runtime-watchdog.md`.
---
## B3. 소유자(윤찬) 결정 — 전건 확정(2026-06-30)
> 7건(평가설계 H1 κ/ICC·환각률, s2s 2차 PoC 채택, 페르소나 음성 모델·권리, fast-loop 외부 API 허용,
> 재귀학습 fine-tuning 범위, 운영 보존기간·에스컬레이션, 추가 축어록 수급 20건/1개월)은 **전건 확정**됐다.
> 결정 상세는 대시보드 "윤찬 결정 상세" 탭, ADR은 `docs/decisions/`(`voice-s2s-poc.md`,
> `backend-node-transition.md`)에 있다. 각 결정의 구현·실측은 후속 build/GATE로 남는다.
### B3-신규 (2026-07-27 전수 E2E 순회에서 제기 → **2026-07-27 소유자 전건 결정 완료**)
> 소유자(윤찬)가 질문 도구로 7건 전건 결정(2026-07-27). 결정 내용과 구현 상태:
- [x] **설정 화면 아바타 변경 UI** — 결정: **설정에 추가**. **구현 완료(RED→GREEN, 대시보드 144차)**.
- [x] **동의 관리·철회 UI와 재동의 흐름** — 결정: **전체 흐름 구현**(설정 동의 섹션 + 철회 +
재동의 경로). **구현 완료(RED→GREEN, 대시보드 144차)**.
- [x] **신규 학습자 첫 추천 난도** — 결정: **기초 우선 추천**(회기 기록 없는 신규 학습자는
기초 난도 페르소나를 첫 추천으로). **구현 완료(RED→GREEN, 대시보드 144차)**.
- [x] **학생 분석 테이블** — 결정: **지금 가상화 도입**. **구현 완료(RED→GREEN, 대시보드 144차)**.
- [x] **저작 모드 좌측 레일 잔존 마크업** — 결정: **정리**(저작 모드에서 레일 미렌더). **구현 완료(RED→GREEN, 대시보드 144차)**.
- [x] **dev-login 재로그인 자동 재승인** — 결정: **현행 유지**(dev/E2E 편의 의도 확정). 코드 변경 없음.
- [x] **감정 밸런스 타임라인 차트**(결함 로그 #21) — 결정: **지금 구현**(평가 데이터 생산→저장→
리드모델→차트 연결). **구현 완료(RED→GREEN, 대시보드 144차)**.
> 근거·상세: `docs/ops/e2e-full-sweep-2026-07-27.md` 발견 결함 로그 #2·#4·#6·#14·#15·#16·#21.
---
## B4. 외부 거버넌스 — 한신대/데이터 steward 서면 증거
- [ ] **C-001 자살사고 케이스 위기 반응 프로토콜 임상 승인** — SAMHSA SAFE-T, NIMH Youth Outpatient
BSSA, NICE NG225, 보건복지부 109를 반영했다. P1 합성 사례 6건·v2 스키마·상태별 canonical fail-closed 판정기와
런타임/carry-over/output `ideation_stage <= 3` 이중 방어를 포함한 기술 사전검증은 완료했다. 생성·스트림
실제 경로 P1 6/6, focused 90 passed, 런타임 패키지·작업트리 SHA 일치까지 확인했고, 개선관리 완료본의
`C-001 외부검토` 시트는 현재 `pending-valid`·`검토`다. 사례별 판정·근거·수정 요구·재검토·검토일 30개,
청소년 특화 답변 6개, 임상 검토자 이름·소속 기관·자격·검토일·결정 근거·서면 증거/해시·검토 프로토콜 버전·
사례 세트 해시가 모두 기록되고 canonical 판정기가 `review_complete=true`를 반환하기 전에는 완료로 닫지 않는다. 검토 원장:
`docs/ops/clinical-crisis-protocol-review-2026-08-27.md`.
- [ ] **한신대 데이터/SSO 거버넌스 게이트** — SSO claim mapping, 추가 축어록 수급, 미성년 원본 활용동의, 개인정보
처리방침. 로컬 골격 `docs/ops/hanshin-data-governance-gate.md` 준비됨, 외부 서면 증거 필요. (소유자 보류 중.)
- [ ] **L1 스택 정합성·저작권 등재 문서 반영** — 백엔드는 Node.js 교체 가능성 우선으로 결정됐지만, 신청서/저작권
등재 문서에는 현재 FastAPI 유지 사유와 계약 우선 전환 계획을 반영해야 한다. 로컬 결정 기록은
`docs/decisions/backend-node-transition.md`; 외부 제출/등재 증거 필요.
---
## Phase 3 파일럿 게이트 (실참여자 필요)
---
## 배포 파이프라인 재정립 + A 경로 (NAS prod 실배포 → E2E 검증) — 2026-09-01 재부팅 후 실행
> **소유자 지시(2026-08-31)**: 배포는 Forgejo(git.chanpaca.net) 중심, NAS Production 대상, github은 private 백업/미러,
> 이 PC는 개발 전용, Cloudflare는 서빙용만·tunnel 제거. **A 경로로 진행**: 실제 prod 배포 준비 → 실배포 → 로그인·작동 E2E 확인까지.
> 2026-09-01 재부팅 후 아래를 순서대로 진행. 지침: `docs/ops/deployment-pipeline.md` · 핸드오프 `docs/ops/handoff-goal-production-2026-08-29.md`.
### 재부팅 직후 (truth 재확인) — **2026-09-01 완료**
- [x] OS/셸/경로/도구 확정, git HEAD `be08c0b5`·Forgejo master 일치·미커밋 71/untracked 27 확인
- [x] live public API health 200(`prod`·db/engine true)·NAS `vignette-prod` 5컨테이너 확인 — `scripts/vignette-pipeline.py --check` ALL OK
- [x] NAS prod 배포 기계장치 확정: `192.168.0.38:/volume1/docker/vignette-prod`(compose+`private/env.nas` immutable image pin), Forgejo http clone(git 컨테이너)→NAS 로컬 docker build로 고정. cloudflared는 NAS 호스트 상주(`private/cloudflared-host.yml`, api-vignette만 ingress) — public 도메인은 이 PC와 완전 분리 확인
### A-1. 배포 후보 GREEN 정리 — **2026-09-01 완료**
- [x] 미커밋 diff를 4개 논리 커밋으로 좁게 stage(회기 연속성 API·케이스 UI·NAS infra·docs/증거), tmp/outputs 제외
- [x] web typecheck·build PASS / API 1090 passed·gateway 71 passed / SSOT checker·design SSOT·api-types PASS
- [x] Forgejo master push, 최종 배포 SHA `dce85620` 고정(github 미러 동기화 포함)
### A-2. NAS prod 실배포 — **2026-09-01 완료**
- [x] prod 전용 절차 실행: pre-deploy dump `cab6826b…`(11,036,928B·TOC 1783)+env 백업+구 이미지 보존(롤백 경로), 외부 볼륨 `pgdata`/`apiuploads` 무접촉
- [x] 중복 활성 회기 14쌍·30행을 최신 유지 정책으로 종료(영수증 `receipts/stale-active-cleanup-20260901.csv`, 삭제 0) 후 마이그레이션 20/21/22 온라인 적용. mig22의 `name[]=text[]` 캐스트 결함 발견→`5a74302e` 교정. master에 게이트웨이 openai provider가 누락된 회귀 발견→구 운영 이미지에서 이식(`dce85620`, 테스트 71 passed)
- [x] readiness: public health `ok·db/engine true·openai OK`, OpenAPI 200, `/auth/me` 401, dev-login 404, 컨테이너 healthy, 데이터 보존(app_user 1380/sessions 618/turns 1743/case_profile 565 전후 동일)
### A-3. 로그인·작동 E2E 검증 (실브라우저) — **2026-09-01 완료 (전건 GREEN)**
- [x] public 로그인 화면 렌더(단일 Google CTA·콘솔 오류는 예상된 401 1건) + OAuth redirect_uri `https://api-vignette.chanpaca.net/auth/callback`·PKCE S256 검증 — headful 브라우저
- [x] 실제 Google 계정 로그인→callback→회기·턴 생성 완료 — 첫 시도에서 NAS env의 OAuth secret 오류(`invalid_client`)를 발견해 동작 실적 secret으로 교정 후 성공. headful 폐루프: 로그인(소유자 직접 입력)→기존 회기 20건 표시(이전 데이터 보존)→활성 회기 가드·시간 만료 fail-closed 확인→stale 회기 정식 종료→**새 사례(fresh case) 회기 생성(mig22 운영 실증)**→pre pulse 잠금→실턴 SSE·AI 내담자 응답→종료→리뷰 생성. DB 실증: sessions 618→619·turns 1743→1745·case_profile 565→566. 증거: `ops/evidence/prod-e2e-review-2026-09-01.png`
- [x] 배포 WEB: Pages production `9224a64b`, 새 entry `index-BFKGXJQi.js` 200·정상 MIME, 직전 entry `index-Bm30Ozeb.js` 200 보존(이전 세대 자산 175개 보존). 관리자 콘솔 렌더·서비스 상태 5/5 확인
- [x] **DB 백업 예약 실행 운영화(1단계)**`db-backup` 사이드카(일일 pg_dump→`pg_restore -l` 검증→원자 게시, 14일 보존)를 정식 스택에 추가, 첫 자동 덤프 11,202,927B 성공. owner 비번-볼륨 불일치도 env와 일치화. 실패 알림·off-host 암호화 복제·restore drill 주기는 후속 게이트로 유지
### 잔여(재부팅 외) — 그대로 유지
- [ ] 실배포 완료 전까지는 아래가 열린 게이트 유지: `public API/DB·아바타 정식 승격`, `G8 실DB/public 사람 게이트`,
`음성 캐스케이드 live`, `DB 백업 운영화`, `vnet.18ka.net live`, `claude_cli↔Anthropic API live`, `Compose infra/.env`,
`B1 티켓 자동 분류`, `C-001`, `한신대 거버넌스`, `L1 등재`, `Phase 3`, 재부팅 후 watchdog smoke/REQ-008
- [ ] **20명 교육용 파일럿 운영 / 효과성·KPI 측정(SUS·자기효능감·κ/ICC·환각률) / 재귀학습 데이터셋 approved 산출 /
개인정보·동의 감사.** 문서·checker·dry-run exporter는 준비됨(`docs/phase3/*`, `scripts/check-phase3-artifacts.py`,
`scripts/export-recursive-dataset.py`, `scripts/export-phase3-kpi.py`). 공식 문항 확정, 통계 검정, 실험/통제군 배정,
실제 approved evidence는 파일럿·steward/legal 검수·IAA 통과 시점의 GATE다.

View file

@ -0,0 +1,353 @@
# Vignette 전체 개선 목표 · 정식 배포 핸드오프 (역사 스냅샷)
> 이 문서는 2026-08-29~30 배포 전 상태의 보존 기록이다. 이후 2026-09-01 NAS 배포와 OAuth 실계정 회기 E2E가 있었으므로 현행 상태를 이 문서로 판단하지 않는다.
> 현재 실행 순서와 승인 경계는 [`docs/HANDOFF.md`](../../HANDOFF.md), 열린 항목은 [`docs/TODO.md`](../../TODO.md), 상태 SSOT는 [`docs/dev_dashboard.html`](../../dev_dashboard.html)를 따른다. 2026-09-01 당시 증거는 [`nas-prod-deploy-2026-09-01.json`](../../ops/evidence/nas-prod-deploy-2026-09-01.json)이다.
작성 시각: 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

@ -0,0 +1,95 @@
# 전 저장소 리팩터 거버넌스 실행 기록 — 2026-07-15
> 상태: **DONE (증거 기반 구조 개선 패스)**
> 방법: `refactor-governance` Edit Pass(P1→P8), 동작 보존 우선
> 권위 상태: `docs/dev_dashboard.html`과 동기화. 기능 로드맵이나 외부 실증 게이트를 대신하지 않는다.
## 1. 목표와 불변 조건
목표는 파일 길이를 기계적으로 줄이는 것이 아니라, 변경 시 서로 어긋날 수 있는 계약·권한·캐시·표시 규칙을 한 소유자로 모으고 반복 IO와 초기 번들 비용을 줄이는 것이었다.
불변 조건:
- 인증/RBAC/RLS, 감사 로그, PII 마스킹, durable/degraded 판정은 바꾸지 않는다.
- API 응답과 생성 OpenAPI 타입, 엔진 packet, DB 스키마의 의미를 바꾸지 않는다.
- 세션 턴 순서, 평가 재시도, 알림 수신자, 화면의 오류 문구와 빈 상태를 보존한다.
- 기존 사용자 작업이 섞인 dirty worktree를 되돌리거나 일괄 포맷하지 않는다.
- 공통화는 오류 순서·캐시 fallback·side effect가 같은 경우에만 한다.
## 2. 감사 범위와 최초 증거
아카이브를 제외한 생산 코드, 테스트, 인프라, 활성 문서를 전수 스캔했다.
| 영역 | 파일/줄 기준 | 중점 검사 |
|---|---:|---|
| `apps/api/app` | 100 files / 45,112 lines | 장기 함수·인자 묶음·SQL 반복·캐시·LLM 감사·세션 IO |
| `apps/web/src` | 74 files / 46,935 lines | 대형 라우트·중복 view model·API 타입·초기 번들·디자인 소유권 |
| `apps/web/e2e` | 25 files / 12,381 lines | 동작/레이아웃/DB 증거 게이트 |
| `scripts`, `infra` | 37 files / 7,430 lines | 런타임 스키마와 운영 경로 중복 |
| 활성 `docs`, `data` | 111 files / 42,955 lines | SSOT drift·검증 숫자·현재/아카이브 경계 |
최초 정적 결과는 Ruff 16건, 생산 코드 중복 15 clones / 327 lines(0.60%), 초기 JS 678.88 kB(gzip 195.75 kB), 초기 CSS 328.49 kB(gzip 51.46 kB)였다. Python 함수 1,903개 중 60줄 이상 135개·인자 7개 이상 26개, TS/TSX 함수 1,426개 중 60줄 이상 61개가 후보였다. 이 후보는 길이만으로 수정하지 않고 호출·중복·소유권 증거를 다시 확인했다.
## 3. 적용한 패치 그룹
| 그룹 | 변경 | 단일 소유자/효과 |
|---|---|---|
| P1 삭제·정적 정리 | Ruff 16건 제거, 미사용 컴포넌트·의존성·죽은 export 정리, Knip 게이트 도입 | 참조 0인 코드와 선언 drift 제거 |
| P2 shape 안정화 | dataset manifest, voice context/prosody/turn/audio를 명시적 input object로 전환 | 긴 positional/keyword 묶음의 의미를 타입 이름으로 고정 |
| P3 계약 SSOT | 평가 write 계약, OpenAPI 생성 타입, `runtime_schema.py`, format/runtime diagnostics를 단일화 | API/DB/runtime/UI 미러 선언 drift 차단 |
| P4 캐시 | KB process cache의 key·수명·invalidate 경로를 한 모듈에 고정 | admin sync 뒤 stale source pack 방지 |
| P5 workflow | KB/persona source workflow, LLM generate+audit, 알림 수신자, managed session sync, admin normalization 공통화 | 같은 side effect·오류 의미를 한 구현으로 통합 |
| P6 성능 | `App.tsx` 전 라우트 lazy loading, 공통 Suspense, 세션 상태/턴 batch read | 초기 payload 감소, missing-evaluation 복구 1+2N query를 3 query로 축소 |
| P7 인프라 | dev runtime schema와 `infra/db/init` 정의 정합, prod DDL fail-closed, E2E fixture/single-run 2단계 실행 | 개발 자동보강과 운영 migration 역할 분리, 단일 DB/engine 포화 방지 |
| P8 UI/디자인 | Surface/AppShell/theme/페르소나 시각 view model SSOT, 대형 화면의 순수 모델·음성 캡처 분리, raw-color 예산 게이트 | 공통 primitive와 화면 예외의 소유권을 테스트 가능한 규칙으로 고정 |
추출된 주요 경계:
- 백엔드: `services/llm_audit.py`, `runtime_schema.py`, 평가/세션/알림/KB의 named contract와 batch loader.
- 프론트: `lib/personaViewModel.ts`, `lib/runtimeDiagnostics.ts`, `pages/admin/dataNormalization.ts`, `pages/persona-studio/model.ts`, `pages/learner-home/model.ts`, `pages/session-review/model.ts`, `pages/session/voiceCapture.ts`.
- 거버넌스: `check:dead-code`, `check:duplication`, `check:design-ssot`, `check:api-types`.
## 4. 전후 측정
| 지표 | 이전 | 현재 | 판정 |
|---|---:|---:|---|
| 생산 코드 중복 | 15 clones / 327 lines / 0.60% | 1 clone / 15 lines / 0.03% | 95.4% duplicated-line 감소, threshold 0.05% 게이트 |
| 초기 JS | 678.88 kB / gzip 195.75 kB | 241.49 kB / gzip 77.13 kB | gzip 60.6% 감소 |
| 초기 CSS | 328.49 kB / gzip 51.46 kB | 28.59 kB / gzip 6.45 kB | gzip 87.5% 감소 |
| `Session.tsx` | 4,002 lines | 3,491 lines + `voiceCapture.ts` | 음성 브라우저 경계 분리 |
| `LearnerHome.tsx` | 2,053 lines | 1,857 lines + 222-line model | 표시 계산을 순수 모델로 분리 |
| API 단위 테스트 | 393 | 400 passed | 신규 구조 회귀 포함 |
| Gateway 단위 테스트 | 27 | 29 passed | packet/model 계약 포함 |
| Playwright 수집 | 214 / 20 files | 215 / 20 files | 현행 목록 동기화 |
## 5. 의도적 비추출과 통제된 예외
1. `session_persistence.py`의 15-line clone 한 건은 유지한다. 두 principal-aware acquire 경로는 표면 구조만 같고 예외 시 cache fallback과 durable 판정이 다르다. 합치면 실패 의미가 숨겨지므로 `jscpd` 전체 threshold 안의 근거 있는 제외다.
2. `Session`, `Admin`, `PersonaStudio`, `Professor`의 라우트 컨테이너는 여전히 크지만 현재 생산 TS/TSX 중복은 0이다. 화면별 상태 전이까지 무리하게 generic hook/component로 만들면 읽기 비용과 prop surface가 늘어난다. 새 기능이 독립 상태·독립 E2E를 가질 때 해당 slice를 추출한다.
3. 세션 dark stage와 아바타/인증 아트의 국소 raw color는 전역 토큰으로 승격하지 않는다. 공통 `ui.css`/`shell.css`는 raw color 0개를 강제하고, 예외 파일은 2026-07-15 기준 수치 이상 증가하지 못하도록 고정 예산을 둔다.
4. 전체 215개 Playwright에는 실제 DB·엔진·provider가 필요한 single-run 시나리오가 섞인다. 기존 direct runner는 로컬에서 12 workers로 세 프로젝트를 동시에 실행해 188 passed/27 resource-timeout을 만들었고, single-run을 뺀 12-worker fixture 단계도 145/166 뒤 21 request timeout을 재현했다. `npm run e2e`를 fixture desktop/mobile `workers=4` 단계 뒤 DB/engine `workers=1` 단계가 시작되는 구조로 바꿨다.
5. 관리자 티켓 큐는 최대 120개 복합 카드를 한 번에 다시 그리며 액션 pending 상태까지 페이지 루트가 소유했다. 카드 렌더 상한을 24개로 두고 나머지는 서버 검색/필터로 탐색하게 했으며, 중복 요청 잠금과 시각 pending은 `TicketActions`가 카드 단위로 소유한다. 네이티브 입력 dispatch와 React 상태 렌더를 분리해 해결/중복 연결 실제 클릭 4건이 desktop/mobile에서 통과한다.
6. Settings 초기 로드는 React 개발 모드 effect 재실행으로 같은 GET 두 개가 경합할 수 있었고, 늦게 끝난 응답이 사용자가 방금 바꾼 엔진 모드·모델을 원래 값으로 덮어썼다. 요청 세대 번호를 추가해 최신 로드만 상태를 반영하고 unmount된 요청은 무효화했다. 기존 engine settings E2E가 실제 PATCH body와 복구까지 고정한다.
7. AI 튜터 DB 영속화는 정상 provider 응답이 독립 실행에서도 138.5초 걸려 기존 150초 테스트 상한과 여유가 11초뿐이었다. 기능 timeout을 숨기지 않고 동일 파일의 장시간 실엔진 기준인 240초로 테스트 예산을 조정했으며, 최종 전체 직렬 실행에서는 2.1분에 ready 응답·source pack·DB history·reload 증거를 모두 통과했다.
## 6. 검증 증거
- `ruff check app` — passed.
- `pytest -q app`**400 passed**, Starlette `python_multipart` PendingDeprecationWarning 1건.
- `pytest -q engine_gateway`**29 passed**, 같은 외부 의존 warning 1건.
- `npm run typecheck` / `check:api-types` / `check:design-ssot` / `check:dead-code` / `check:duplication` — passed.
- `npm run build` — 109 modules, initial JS 241.49 kB(gzip 77.13 kB), initial CSS 28.59 kB(gzip 6.45 kB).
- `npm audit --audit-level=high` — 0 vulnerabilities.
- `npx playwright test --list`**215 tests / 20 files**.
- `npm run e2e:parallel`**166 passed** (desktop 83 + mobile 83, workers=4).
- `npm run e2e:single-run`**49 passed** (DB/engine/provider 직렬, 23.2분).
- 전체 Playwright 최종 판정 — **215 / 215 passed**.
- `git diff --check` — passed; checkout 정책에 따른 LF→CRLF 경고만 존재.
## 7. 앞으로의 변경 규칙
- 새 DTO/packet은 OpenAPI·schema·named adapter 중 한 곳만 원본으로 둔다.
- 새 캐시는 key, SoR, invalidate, TTL, 실패 시 durable/degraded 의미를 함께 정의한다.
- 새 공통화는 `check:duplication`의 실제 clone 또는 둘 이상의 동일 side effect가 증거일 때만 한다.
- 새 페이지 색은 먼저 의미 토큰을 사용하고, 국소 예외면 이유와 raw-color 예산을 함께 갱신한다.
- 라우트 컨테이너 추출은 독립 입력/출력, 독립 상태, 독립 테스트가 생긴 뒤 한다. 파일 길이만으로 분리하지 않는다.