세션 평가·라이브코치·교수자 분석 라운드 마감 + 문서 정리 + 코드품질 리팩터

- 누적 작업트리 커밋: 회기 평가 복구·durable 저장, 라이브 코치 이력/근거, 교수자 학생분석, 음성 비언어 메타, PII 마스킹, 운영 티켓/헬스 등
- 문서: 완료 기록 docs/archive/ 냉동 보관, docs/ 단일 인덱스(docs/README.md)+통합 TODO(docs/TODO.md)로 정리
- 리팩터(행위 보존): Stage enum SSOT(taxonomy 소유·state_machine re-export), store recent/masked_turns 중복 제거, speaker_ko_label 단일 헬퍼, _list_sessions N+1 제거(state/turns 배치 + 턴평가 하이드레이션 배치)
- 검증: 백엔드 pytest 352 passed, _list_sessions E2E chromium-single-run 2 passed
This commit is contained in:
Yun Chan 2026-07-02 02:50:36 +09:00
parent 7c41c3ce79
commit 778e8526d4
108 changed files with 6457 additions and 455 deletions

96
docs/README.md Normal file
View file

@ -0,0 +1,96 @@
# Vignette 문서 인덱스 (docs/ 통합 목차)
> **여기가 문서의 단일 진입점이다.** `docs/` 안의 모든 정보를 카테고리로 묶어 링크한다.
> 무엇을 찾든 여기서 시작하라. 상태·결정·로드맵의 권위 기준은 SSOT 대시보드, 남은 작업은 통합 TODO가 소유한다.
## 🎯 먼저 볼 것
| 문서 | 역할 |
|---|---|
| **[`dev_dashboard.html`](./dev_dashboard.html)** | **SSOT(단일 진실 공급원)** — 상태·검증 증거·결정·로드맵의 권위 기준 |
| **[`TODO.md`](./TODO.md)** | **할 일(안된 것들) 통합** — 흩어진 열린 작업을 주제별로 모은 인덱스 |
| [`../AGENTS.md`](../AGENTS.md) | 에이전트 작업 지침 원본(OS 선파악·증거 정직성·SSOT 동기화·이미지/아바타 파이프라인) |
## 🗺 원천문서·갭 로드맵
| 문서 | 역할 |
|---|---|
| [`guides/source-docs-and-gaps.md`](./guides/source-docs-and-gaps.md) | 원천문서·갭 로드맵 요약(대시보드와 동기화) |
| [`ops/source-docs-gap-analysis-2026-06-26.md`](./ops/source-docs-gap-analysis-2026-06-26.md) | 원천문서 갭 분석 상세 근거(C1~L1) |
| [`ops/backlog-2026-06-26.md`](./ops/backlog-2026-06-26.md) | 운영 백로그 — 열린 항목만 얇게(B1~B4·Phase3 게이트) |
## 📐 근본 설계·비전 (reference — 완료 기록 아님)
| 문서 | 역할 |
|---|---|
| [`MASTERPLAN.md`](./MASTERPLAN.md) | 통합 기준 설계·비전·아키텍처·분류체계·Phase 로드맵 |
| [`redteam/MASTERPLAN_REVISIONS.md`](./redteam/MASTERPLAN_REVISIONS.md) | 마스터플랜 재설계 패치·의사결정 정정(§9 열린 결정 포함) · 대상 결함 [`redteam/REDTEAM_FINDINGS.md`](./redteam/REDTEAM_FINDINGS.md) |
| [`MEMORY_KNOWLEDGE_PERSONA_DESIGN.md`](./MEMORY_KNOWLEDGE_PERSONA_DESIGN.md) | 메모리·지식·페르소나·3-AI 정보비대칭 통합 아키텍처(스키마 DDL 포함) |
| [`taxonomy.md`](./taxonomy.md) | 상담 발화 라벨 분류체계 SoT(기법/내담자상태/슈퍼바이저논평) |
| [`DESIGN_CONCEPT.md`](./DESIGN_CONCEPT.md) | 디자인 시스템·색토큰·아바타·세션UX·3역할 대시보드 SSOT |
| [`design-concepts/prompts/vignette-design-prompts-2026-06-27.md`](./design-concepts/prompts/vignette-design-prompts-2026-06-27.md) | 화면 시안 이미지 생성 프롬프트·공통 시각 방향 |
## 🛠 개발·운영 가이드
| 문서 | 역할 |
|---|---|
| [`guides/local-development.md`](./guides/local-development.md) | 로컬 개발 환경 구축·실행 (1-커맨드 dev 스택) |
| [`guides/architecture.md`](./guides/architecture.md) | 시스템 아키텍처(엔진/오케스트레이터/저항/마스킹/음성/평가/데이터) |
| [`guides/testing.md`](./guides/testing.md) | 테스트·검증(pytest, typecheck, Playwright E2E 게이트) |
| [`DEPLOYMENT.md`](./DEPLOYMENT.md) | docker compose 배포·엔진 어댑터·환경변수·외부공개 운영 |
| [`HANDOFF.md`](./HANDOFF.md) | 최신 세션 인수인계 — 현재 상태·남은 외부 게이트·다음 우선순위 |
## ⚙️ 운영 런북·게이트 트래커 (`ops/`)
| 문서 | 역할 |
|---|---|
| [`ops/public-runtime-watchdog.md`](./ops/public-runtime-watchdog.md) | 공개 런타임 워치독 설치·복구 절차(런북) |
| [`ops/tailscale-vnet-runtime-2026-06-27.md`](./ops/tailscale-vnet-runtime-2026-06-27.md) | Tailnet 런타임 + 공개 vnet 외부 게이트 추적 |
| [`ops/hanshin-data-governance-gate.md`](./ops/hanshin-data-governance-gate.md) | 한신대 데이터/SSO 외부 증거 게이트 체크리스트 |
| [`ops/code-quality-research-2026-06-26.md`](./ops/code-quality-research-2026-06-26.md) | 코드품질 분석 + 미실행 리팩터 로드맵 |
| [`ops/postgres-rls-audit-smoke.md`](./ops/postgres-rls-audit-smoke.md) | RLS·감사로그 격리 스모크 절차(재현 런북) |
| [`ops/engine-rss-smoke-2026-06-28.md`](./ops/engine-rss-smoke-2026-06-28.md) · [`ops/resistance-openness-db-smoke-2026-06-28.md`](./ops/resistance-openness-db-smoke-2026-06-28.md) | 대시보드가 인용하는 live 실측 증거 |
| [`ops/layout-research-2026-06-27/persona-dashboard-layout-guideline.md`](./ops/layout-research-2026-06-27/persona-dashboard-layout-guideline.md) | 페르소나 스튜디오·대시보드 레이아웃 원칙 |
## 🧩 의사결정 기록 (ADR — `decisions/`)
| 문서 | 역할 |
|---|---|
| [`decisions/backend-node-transition.md`](./decisions/backend-node-transition.md) | 백엔드 언어 방향(FastAPI 유지·Node 계약 우선 전환) |
| [`decisions/voice-s2s-poc.md`](./decisions/voice-s2s-poc.md) | 음성 s2s 2차 PoC 채택 판단 기준 |
## 🧪 Phase 3 파일럿 (forward — 아직 미실행)
| 문서 | 역할 |
|---|---|
| [`phase3/README.md`](./phase3/README.md) | Phase3 파일럿 게이트 인덱스·증거 루트 |
| [`phase3/pilot-runbook.md`](./phase3/pilot-runbook.md) | 20명 파일럿 진입/완료 게이트·운영 절차 |
| [`phase3/kpi-measurement-plan.md`](./phase3/kpi-measurement-plan.md) | KPI 정의·리포트 형식·측정 방법 |
| [`phase3/dataset-export-manifest.md`](./phase3/dataset-export-manifest.md) | 재귀학습 데이터셋 export 매니페스트·승인 게이트 계약 |
| [`phase3/privacy-consent-audit.md`](./phase3/privacy-consent-audit.md) | 동의·철회·프라이버시 감사 체크리스트 |
## 🖼 아바타 파이프라인
| 문서 | 역할 |
|---|---|
| [`ops/handoff-avatar-seoyeon-2026-06-27.md`](./ops/handoff-avatar-seoyeon-2026-06-27.md) | 서연(P1) 아바타 작업 핸드오프(별도 세션 진행) |
| [`avatar-art/personas/README.md`](./avatar-art/personas/README.md) | P4~P7 Live2D 파츠 생성 규칙 |
| `avatar-art/` | 페르소나별 파츠·재현 스크립트·QA 자산(활성 파이프라인) |
## 🗄 아카이브 (읽지 않는다)
| 문서 | 역할 |
|---|---|
| [`archive/README.md`](./archive/README.md) | **냉동 보관소** — 완료 기록·실행된 계획·일회성 스냅샷. 현재 상태 근거로 삼지 않는다 |
## 📦 그 외 자산 (문서가 아니라 참조 자산)
`design-verification/`(실제 렌더 검증 캡처) · `mockups/`(디자인 목업) · `design-concepts/generated/`(시안 PNG) ·
`voice-art/`(음성 PoC 샘플·스크립트) · `golden_schema.jsonl`(평가 골든셋 스키마). 각 문서가 필요 시 참조하며,
단독으로 읽는 계획 정보는 아니다.
---
> 유지 원칙: 완료된 작업 기록이 다시 쌓이면 `archive/`로 옮겨 이 인덱스와 활성 문서를 얇게 유지한다.
> 상태·결정 변경은 별도 문서로만 남기지 말고 **SSOT 대시보드**에 반영·동기화한다.
> 새 문서를 만들면 이 인덱스에 한 줄 추가하고, 새 열린 작업은 [`TODO.md`](./TODO.md)에 반영한다.

88
docs/TODO.md Normal file
View file

@ -0,0 +1,88 @@
# Vignette 할 일 (안된 것들) — 통합 TODO
> **한 곳에서 보는 "남은 작업" 인덱스.** 여러 문서에 흩어져 있던 열린 작업을 주제별로 모았다.
> **권위 기준은 SSOT `dev_dashboard.html`**, 열린 게이트 추적은 `ops/backlog-2026-06-26.md`(얇은 현행본)다.
> 이 파일은 그 둘을 **집계·링크**하는 뷰이며, 상태가 어긋나면 대시보드를 기준으로 맞춘다.
> 완료된 작업 기록은 `archive/`(냉동 보관)에 있고 평상시 읽지 않는다.
>
> 태그: **[환경]** 이 워크스테이션에서 증거 생산 불가 · **[외부]** 기관/소유자 서면·결정 필요 ·
> **[구현]** 코드로 진행 가능 · **[임상]** 임상팀(구훈정·어유경) 외부 콘텐츠 소유.
---
## A. 배포·공개 런타임 게이트 [환경]
> 출처: `ops/backlog-2026-06-26.md` B2 · 대시보드 운영 탭 · `ops/tailscale-vnet-runtime-2026-06-27.md` · `ops/public-runtime-watchdog.md` · `HANDOFF.md`
- [ ] **공개 Google OAuth 실제 `/turn` proof** — 로그인 계정으로 `storageState` 캡처 → `E2E_PUBLIC_AUTH=1` +
`chromium-public-auth` 1회 통과. 실행 명령은 대시보드 "다음 실행 명령"/`HANDOFF.md`. (소유자 보류 중)
- [ ] **실배포 `infra/.env` owner-secret + real preflight**`APP_DB_PASSWORD`, `OAUTH_GOOGLE_CLIENT_ID/SECRET`,
`OPENAI_API_KEY`, `SESSION_SECRET`, prod-safe engine/voice flags 주입 후 `check-deploy-preflight.py` DB 모드 + 배포지 health.
- [ ] **`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)
- [ ] **재부팅 후 watchdog smoke** — 실제 Windows 재부팅 후 엔진/API/터널 자동 복구 + public `/turn` 실측.
DNS 개통 후 `api-vnet.18ka.net``-AdditionalPublicHealthUrls`로 명시 추가.
- [ ] **음성 캐스케이드 live** — 실제 Deepgram interim/final WSS, 물리 마이크, 공개 WSS, 50분 양방향 장시간 실측.
- [ ] **claude_cli ↔ Messages API 폴백 동일성**`ANTHROPIC_API_KEY` 필요.
## 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 폴백, 전송 가드(데이터주권·로그·동의)·관측.
- [ ] **대체 TTS provider 선정·통합** — 상업 라이선스 명확한 provider. dev sample(`VIGNETTE_VOICE_POC_SAMPLE_TTS`)과 서비스 경로 분리 유지.
- [ ] **재귀학습 fine-tuning 파이프라인** — 동의/데이터셋 게이트 충족 후. few-shot 자동갱신은 선행 가능.
- [ ] **헬스 retention 적용·검증** — raw 90일 / rollup 365일. escalation/그룹 자동화는 미도입 고정.
## 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. 코드 구조는 선제 구축, 문안·기준은 외부 정의.
- [ ] C1 사례개념화 **확정 루브릭 콘텐츠** + AI 추출/채점 calibration (현재 scaffold_only).
- [ ] CBT 체인·이론부합 루브릭.
- [ ] 위기개입 프로토콜 임상 문안.
- [ ] 평가 골든셋 콘텐츠.
## G. 운영 후속 [구현] (비차단)
> 출처: 백로그 B1
- [ ] **운영 티켓 자동 분류·처리** — Claude Recipe headless 자동 수정 후보, 관리자 승인 후 이슈 등록·PR/작업 스레드 생성,
처리 결과 audit trail 확장. 자동 수정은 운영자 승인 전까지 실행하지 않는다.
---
## 지금 임계경로 (한 줄)
납품(공개 데모)의 핵심 블로커는 **A(공개 `/turn` proof + 배포 secrets)**. 그 다음이 **B(한신대 거버넌스)**와
**C(Phase 3 파일럿)**. D~G는 품질·확장 작업으로 게이트가 아니다. 상세 상태·검증 증거는 SSOT 대시보드가 소유한다.

46
docs/archive/README.md Normal file
View file

@ -0,0 +1,46 @@
# docs/archive — 냉동 보관소 (읽지 않아도 되는 완료 기록)
> **에이전트 주의: 이 디렉터리는 평상시 작업에서 읽지 않는다.**
> 여기에는 **이미 완료·검증된 작업의 변경 로그**, 실행이 끝난 구현/리팩터 계획,
> 일회성 smoke·RSS·RLS 실측 기록, 특정 날짜 핸드오프, 완료된 재설계의 이미지 생성
> 프롬프트·리서치 스냅샷만 모아 둔다. 이는 감사·회고용 이력이며 현재 상태의 근거가 아니다.
## 왜 아카이브했나
작업 진행에 따라 백로그와 ops 문서에 완료된 작업의 검증 로그가 계속 쌓여
"지금 무엇이 열려 있는가"를 읽어내기 어려운 노이즈가 됐다. 완료 기록을 여기로 옮겨
활성 문서를 얇게 유지한다. (2026-07-02 정리)
## 현재 상태·열린 작업은 어디서 보나
- **상태·검증 증거·결정·로드맵의 SSOT**: `docs/dev_dashboard.html`
- **열린 작업(외부/환경 게이트) 추적**: `docs/ops/backlog-2026-06-26.md` (얇게 정리된 현행본)
- **원천문서 갭 요약**: `docs/guides/source-docs-and-gaps.md`
여기 있는 문서를 근거로 현재 상태를 판단하지 마라. 필요하면 이력 확인 용도로만 참고한다.
## 색인 (열어보지 않아도 무엇인지 알도록)
**백로그 전체 이력**
- `ops/backlog-2026-06-26-full-history.md` — 구 백로그 전체본. "최신 동기화 추가" 변경 로그, "이번 세션에
닫은 것", B1~B3 `[x]` 완료 항목의 상세·검증 로그. 열린 항목은 `docs/ops/backlog-2026-06-26.md` 현행본이 소유.
**완료된 날짜 핸드오프**
- `ops/layout-redesign-handoff-2026-06-26.md` — 2026-06-26 레이아웃 재설계 인수인계. 구현·E2E·시각 수용 종료.
- `ops/handoff-2026-06-27.md` — 2026-06-27 세션 인계. 위기게이트 109·교수자 추적 UI 등 CRITICAL 착수안(구현 완료).
**실행 완료된 구현/리팩터 계획**
- `ops/dark-ui-refresh-implementation-plan-2026-06-28.md` — 다크 UI v2 시안→React/CSS 전환 계획·실행 기록.
- `ops/refactor-rag-patches-2026-06-26.md` / `.json` — 리팩터 4종·KB 시드 적용 전 충돌분석·패치 스펙(적용 완료).
- `ops/layout-research-2026-06-28/` — 레이아웃 정렬 라운드 완료 리포트 + before/after·final-gate 캡처.
**완료된 재설계의 이미지 생성 프롬프트**
- `design-concepts/prompts/learner-home-dashboard-redesign-2026-06-27.md` — 학습자 홈 재설계 프롬프트(반영 완료).
- `design-concepts/prompts/learner-history-archive-redesign-2026-06-28.md` — 학습자 기록/아카이브 재설계 프롬프트(반영 완료).
- `design-concepts/prompts/vignette-dark-ui-refresh-2026-06-28.md` — 다크 UI 컨셉 이미지 재생성 프롬프트(산출 완료).
**일회성 PoC 기록**
- `voice-art/bimec-persona-voice-poc-2026-06-27.md` — P1 서연 Higgs v3 무참조 음성 PoC(스모크 완료, 대체 provider 채택으로 초과됨).
> 참고: 이 아카이브 문서 내부의 상대 링크는 이동으로 인해 일부 깨질 수 있다(냉동 기록이므로 보정하지 않음).
> 활성 문서(대시보드·가이드·현행 백로그)의 링크는 이동에 맞춰 갱신했다.

View file

@ -0,0 +1,178 @@
# Vignette 백로그 — 2026-06-26
이 세션(레이아웃 시각 수용 마감 + live 운영 증거 라운드) 이후 남은 항목을 한곳에 모은다.
대시보드(`docs/dev_dashboard.html`)의 상태와 일치한다. 운영 원칙상 가짜 증거로 DONE 표기하지 않으므로,
실증/외부 의존/소유자 결정이 필요한 항목은 여기서 추적한다.
최신 동기화(2026-06-29): M2 다회기 케이스 아크는 `TurnMemory` 값 객체로 턴 메모리 전달 경계를 줄였고, `DigestQualityAssessment`/`SessionDigestWorkerOutcome`로 LLM digest 후보 local quality harness를 추가했다. 이번 라운드에서는 `session_digest_worker.py``CompressionJob`→Node-compatible `GenerateRequest` 변환, 주입형 engine/audit 호출, accepted-only `session_summary`/`case_profile` 적용 계획과 one-shot DB loader 경계를 소유한다. `scripts/run-session-digest-worker.py`는 metadata-only dry-run/apply runner를 제공하고, API는 `SESSION_DIGEST_WORKER_ENABLED=false` 기본값에서 opt-in일 때만 세션 종료 뒤 background worker를 예약한다. loader/apply는 `compressed_by IS NULL` CAS로 이미 압축된 세션 재실행 race를 막고, engine 호출은 DB transaction 밖에서 수행한다. loader는 persisted fallback summary와 client-visible `text_masked` transcript만 재구성하며 raw `text`, evaluator-only turn, CCD, end_state를 압축 prompt에 넣지 않는다. `scripts/check-dev-dashboard-ssot.py`는 대시보드 카드 상태와 M2 30/87 검증 수치, stale DONE/GATE 문구를 guard한다. `recall_summary`/`pinned_facts`/`recent_turns`/`kb_behavior_cues``TurnContext.memory`에 보관되고, `prepare_turn(memory=...)`에서 마스킹된 뒤 `build_turn_messages(memory=...)`로 전달된다. auth managed-user upsert 입력도 `ManagedUserUpsertInput`으로 단일화했지만, 이는 내부 호출 경계 정리이며 외부 API·온보딩 정책·권한 모델 변경은 아니다. Python engine gateway의 `provider/model` 응답 메타 규칙은 helper로 모았고, `GatewayPromptParts`가 current-turn `system_prompt`/`user_payload` 분해 경계를 소유한다. Node conformance runner는 `gateway-default`가 request default-routing sentinel이며 response/done model은 resolved provider model이어야 함을 검증한다. 검증은 `py_compile`, runner `--help`, dashboard SSOT checker PASS, M2 digest worker + memory focused 30 passed, M2 주변 회귀 87 passed, M2 local harness 20 passed, M2/voice/evaluation focused 115 passed, auth/admin/session focused 103 passed, gateway contract 27 passed, backend focused 120 passed, Node conformance OK, `npm run check:api-types`. 단, 실 provider 장시간 운영·임상 골든셋 품질평가·재압축은 계속 후속 GATE다.
최신 동기화 추가(2026-06-29): 관리자·교수자 메일링 시스템 1차가 추가됐다. 가입 승인 요청과 회기 검토 요청은 `app.notification_event`/`app.notification_delivery`에 idempotent 큐로 남기고, SMTP 설정이 있을 때만 실제 메일을 발송한다. 메일 본문은 Vignette 톤앤매너를 따르되 축어록·평가 전문을 포함하지 않고 `/admin/users`, `/teach/session/:sessionId/review` 딥링크만 제공한다.
최신 동기화 추가(2026-06-30): 기본 역할이 learner로 복원되는 슈퍼관리자 계정이 타 학습자 회기 리뷰 딥링크에서 `session does not belong to user` 403에 막히던 문제를 수정했다. 리뷰 조회는 본인 학습자 접근을 우선 유지하고, 실패 시 슈퍼관리자만 admin 감독자 읽기로 재시도한다. 프론트는 동일 403뿐 아니라 이미 `teacherReview`가 포함된 성공 응답도 감독자 권한 사용자에 한해 `/teach/session/:sessionId/review`로 정규화한다. 일반 learner 및 단순 `admin_access` 위임 계정의 타인 세션 접근 차단은 유지했다. 검증: `app/test_rbac_idor.py` 11 passed, RBAC/auth/teacher focused 47 passed, supervisor route E2E 2 passed(`PLAYWRIGHT_PORT=5176`), `npm run typecheck`, `npm run build`.
최신 동기화 추가(2026-06-30): `docs/dev_dashboard.html` 자체를 레드팀 검증 대상으로 추가했다. `scripts/test_dev_dashboard_ssot.py`는 SSOT checker import/stateless/owner parser를 unit으로 검증하고, `apps/web/e2e/dev-dashboard.spec.ts`는 local file, `127.0.0.1`, `localhost` origin에서 필터 오순서·빠른 재시도·키보드·details/tabs·hash anchor·링크/이미지 무결성·desktop/mobile overflow를 E2E로 확인한다. 같은 라운드의 라이브 점검에서 public API `https://api-vignette.chanpaca.net/health``/personas`가 Cloudflare 530/error code 1033으로 실패하는 것을 발견했다. 원인은 Docker Desktop 중단으로 `vignette-dev-db`가 꺼진 상태였고, Docker Desktop/DB 재기동 뒤 `scripts/start-public-runtime.ps1`를 다시 실행해 public health 200, unauth `/personas` 401, auth config 200, Google login start 302, watchdog healthy: engine, api, web-preview, cloudflared, public-api까지 복구했다. 검증: dev-dashboard E2E redteam 10 passed, SSOT unit 3 OK, dashboard checker PASS.
최신 동기화 추가(2026-07-01): public API 장애 이력은 복구 상태로 재분류했다. 2026-06-30과 2026-07-01 17:26 재확인에서 Docker/DB 중단으로 Cloudflare 530이 관측됐지만, 현재 실측 기준 `https://api-vignette.chanpaca.net/health``status=ok`, `db=true`, `engine=true`이고 `https://vignette.chanpaca.net/login`은 200, 비로그인 `GET /personas`는 401을 반환한다. 따라서 `vignette.chanpaca.net`/`api-vignette.chanpaca.net` 런타임 health는 현재 BLOCK이 아니다. 남은 게이트는 공개 Google OAuth 실제 계정 `/turn` smoke, `vnet.18ka.net`/`api-vnet.18ka.net` DNS와 Google redirect 등록, 재부팅 후 watchdog 복구 실측이다. 로컬 dirty worktree 변경은 아직 배포된 상태가 아니므로 배포 전 별도 commit/push/restart/live 확인이 필요하다.
최신 동기화 추가(2026-07-01): 하린/P6 종료 회기의 `SUPERVISOR AI 평가 실패`는 평가가 실행되지 않은 문제가 아니라 102분/138턴 축어록에 대한 회기말 평가 background task가 기존 45초 하드캡에서 timeout된 뒤 `TimeoutError`의 빈 문자열이 그대로 저장된 문제였다. 회기말 평가는 `SESSION_EVALUATION_TIMEOUT` 또는 `ENGINE_TIMEOUT`을 사용하고, 빈 예외 문자열은 타입/명시 메시지로 저장한다. 교수자 재평가 API는 실패 결과를 저장한 뒤 502/503으로 노출하고, 프론트는 종료 실패 alert, 평가 대기 polling, 교수자 `AI 평가 재시도` 버튼을 제공한다. 재시도 실패가 UI에서 조용히 사라지지 않고 기존 실패 리뷰와 새 실패 사유를 유지하는지, 저장된 평가 실패 row가 read-model에서 `평가 실패`와 사유로 노출되는지도 회귀 테스트로 고정했다. 모바일 코칭 모드에서 우측 컬럼 숨김 때문에 AI 튜터 카드가 보이지 않던 회귀도 수정했다. 검증: `python -X utf8 -m pytest app/ -q` 297 passed, `npm run check:api-types`, `npm run typecheck`, `npm run build`, `session-review` desktop/mobile 16 passed, `session-mvp` desktop/mobile/single-run 5 passed, `session-layout` desktop/mobile 8 passed, `layout-visual-gate` 9 passed.
최신 동기화 추가(2026-07-01): 교수자 큐/학생 분석 요약에서 수동 검토 상태만 내려가 `평가 실패``검토 대기`에 묻히던 사일런트 오류를 수정했다. `TeacherSessionSummary`는 이제 `app.session_evaluation``evaluation_status`, `review_ready`, `supervisor_state`, `evaluation_error`를 별도 필드로 전달하고, `Professor` 화면은 AI 평가 상태 배지와 수동 검토 상태 배지를 분리해 표시한다. 리뷰 상세에서는 자동 평가가 아직 `평가 대기` polling 중일 때 수동 `AI 평가 재시도` 버튼을 숨기고, `평가 실패`에만 재시도를 노출한다. 회기 종료 자동 평가와 교수자 수동 재평가가 evaluator에 넘기는 masked turn `seq`도 1-based 공유 helper로 맞췄다. 검증: `py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_eval_routes.py app/test_teacher_dashboard.py app/test_session_turn_persistence.py app/test_notifications.py -q` 54 passed, `app/test_teacher_dashboard.py` 10 passed, `app/test_eval_routes.py` 6 passed, `npm run generate:api-types`, `npm run check:api-types`, `npm run typecheck`, `PLAYWRIGHT_PORT=5236 npx playwright test e2e/session-review.spec.ts e2e/teacher.spec.ts --project=chromium-desktop --workers=1 --grep "manual AI retry|retry a failed AI session evaluation|retry fails|failed AI session evaluation"` 4 passed.
최신 동기화 추가(2026-07-01): 본래 E2E goal의 AI 평가 축을 계속 추적하며, 교수자 명시 재평가가 실제 DB/API/엔진 경로에서 durable `app.session_evaluation` row를 저장하는지 별도 `@single-run`으로 검증했다. 오래된 종료 회기가 evaluation row 없이 남으면 기존에는 `평가 대기`로 조용히 고착될 수 있었으므로, read-model은 `SESSION_EVALUATION_TIMEOUT`/`ENGINE_TIMEOUT` + grace 이후 이를 `평가 실패``AI 평가 재시도가 필요합니다`로 표면화한다. E2E 문서도 route fixture UI 회귀와 DB-backed 증거를 분리하도록 갱신했다. 검증: `PLAYWRIGHT_PORT=5237 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1 --grep "explicit teacher session reevaluation"` 1 passed, `py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_session_turn_persistence.py -k "session_evaluation or stale_missing" -q` 3 passed, `npx playwright test --list` 현재 181 tests / 19 files.
최신 동기화 추가(2026-07-01): 위 read-model 표면화만으로는 프로세스 재시작/태스크 유실 뒤 실제 평가가 복구되지 않는 공백이 남아, startup session evaluation recovery를 추가했다. API 기동 후 `SESSION_EVALUATION_TIMEOUT`/`ENGINE_TIMEOUT` + grace보다 오래된 종료 회기 중 client-visible turn은 있지만 `app.session_evaluation` row가 없는 세션을 evaluator AI context로 조회하고, 기존 session-end 평가 생성 경로를 재예약한다. 같은 session_id의 동시 background 평가는 process-local in-flight set으로 중복 실행을 막는다. 자동 종료 평가 DB-backed E2E도 같은 row가 교수자 리뷰뿐 아니라 `/teacher/dashboard``evaluation_status=ready`, `review_ready=true`, `supervisor_state=평가 완료`로 반영되는지 확장했다. 검증: `py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_notifications.py app/test_session_turn_persistence.py app/test_evaluation_persistence.py -k "session_evaluation or missing_session_evaluation or scheduled_session_evaluation" -q` 16 passed, `npm run typecheck`, `PLAYWRIGHT_PORT=5238 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1 --grep "finishes session end evaluation"` 1 passed.
최신 동기화 추가(2026-07-01): 실제 브라우저 SSE stream 저장 경로를 E2E로 추가 검증했다. 기존 `session-persistence.spec.ts`는 real API/DB를 쓰지만 `/api/sessions/{id}/turn` 직접 호출이라 UI의 `openSessionStream()``/sessions/{id}/stream` → 저장 finalize 경로를 타지 않았다. 새 `@single-run` 테스트는 `db=true`, `engine=true` health 조건에서 Session UI로 회기 시작, 학습자 발화 전송, stream 완료 대기, DB-backed `/review` learner/client 축어록 확인까지 수행한다. 이어서 AI 튜터 코칭 카드가 생성만 되고 `app.live_coach_events` 저장 실패가 묻히는 silent failure를 막기 위해 `POST /sessions/{id}/live-coach`의 저장 실패를 503으로 표면화하고, 실제 UI 코칭 모드 → DB-backed `/live-coach` 이력 → 재로딩 후 `C` 마커 유지까지 E2E로 고정했다. crisis `safety`/`done.conversation_stopped` 또는 음성 `reply.conversation_stopped`도 빈 응답으로 일반화하지 않고 109 안전 게이트를 유지하도록 수정했다. 검증: `python -X utf8 -m pytest -p no:cacheprovider app/test_session_turn_persistence.py -q` 27 passed, `PLAYWRIGHT_PORT=5193 npx playwright test e2e/session-mvp.spec.ts --project=chromium-single-run --workers=1` 2 passed, 최신 `PLAYWRIGHT_PORT=5202 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1` 6 passed, `npm run typecheck`, `npx playwright test --list` 현재 160 tests / 18 files.
최신 동기화 추가(2026-07-01): AI 튜터 live coach가 엔진/structured payload 실패 때 200 `status=degraded` 규칙 기반 제안을 반환하는 것은 의도된 fallback이지만, 이를 정상 AI 코치 응답처럼 보이게 두면 silent error가 된다. Session UI는 degraded suggestion을 `대체 코칭`/`AI 응답 대체`로 카드, 근거 모달, 이력 모달, 턴 `C` 마커 title에 노출한다. DB-backed `session-persistence.spec.ts`는 실제 엔진 응답의 `status=ready`, `latency_ms>0`, source metadata 저장/재로딩을 확인하고, fixture `session-mvp.spec.ts`는 degraded fallback 표시와 이력 보존을 별도 UI 회귀로 고정한다. backend live coach 회귀는 저장된 history payload에도 `suggestion.status=degraded`가 남는지 확인한다. 저장 실패는 기존대로 503 fail-closed이며, degraded fallback과 DB 저장 실패를 같은 성공으로 취급하지 않는다. 검증: `npm run typecheck`, `py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_session_turn_persistence.py -k "live_coach" -q` 5 passed, `PLAYWRIGHT_PORT=5240 npx playwright test e2e/session-mvp.spec.ts --project=chromium-desktop --workers=1 --grep "AI tutor coaching|degraded AI tutor"` 2 passed, `PLAYWRIGHT_PORT=5241 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1 --grep "AI tutor coaching history"` 1 passed, `npx playwright test --list` 현재 181 tests / 19 files.
최신 동기화 추가(2026-07-01): 본래 E2E goal의 원천문서 gap 축을 이어서 H4/C2 silent failure 후보를 추가로 고정했다. 새 DB-backed `@single-run`은 실제 Session UI stream으로 한국어 이름·기관·전화번호가 포함된 발화를 보내고, `GET /sessions/{id}` 상세와 `/review` 모두 raw 값 없이 `[NAME]`/`[ORG]`/`[PHONE]`으로 마스킹되는지 확인한다. 같은 focused 라운드에서 위기 발화는 교수자 검토용 learner turn으로 저장되지만 client AI 응답 turn은 저장되지 않고, DB-backed 교수자 안전 큐 109 알림이 유지되는지도 검증했다. 검증: `PLAYWRIGHT_PORT=5244 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1 --grep "Korean PII|crisis safety event"` 2 passed, `npm run typecheck`, `npx playwright test --list` 현재 181 tests / 19 files.
최신 동기화 추가(2026-07-01): fast-loop 평가 실패가 neutral 점수나 빈 피드백처럼 조용히 보이는 후보를 닫았다. 평가 훅 예외는 상담 턴 저장을 막지 않되 `evaluation.error` payload로 남고, 리뷰 read-model은 해당 학습자 발화에 `턴 평가 실패` 노트를 표시한다. evaluator가 반환하는 `engine_error`/`eval_error`/`parse_error`도 원문 예외 메시지를 사용자-facing payload에 싣지 않으며, `session_metrics`는 error가 있는 fast-loop 결과를 성장 점수와 최근 피드백에서 제외해 실패가 0.5 neutral 평가로 집계되지 않게 했다. 추가로 실제 실패 row를 만든 뒤 교수자 리뷰 UI의 `AI 평가 재시도` 버튼 클릭이 실제 `POST /eval/sessions/{id}/reevaluate` → durable ready row → `/review` `평가 완료``/teacher/dashboard` ready 상태까지 이어지는지 DB-backed E2E로 고정했다. 검증: `py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_session_turn_persistence.py -k "fast_loop_evaluation" -q` 2 passed, `py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_learner_dashboard.py -k "failed_fast_loop" -q` 1 passed, `npm run typecheck`, `PLAYWRIGHT_PORT=5245 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1 --grep "manual AI evaluation retry"` 1 passed, `npx playwright test --list` 현재 181 tests / 19 files.
최신 동기화 추가(2026-07-01): 단일 턴 교수자 재평가가 응답 바디에서만 보이고 이후 리뷰/분석에는 반영되지 않는 silent failure 후보를 닫았다. `POST /eval/sessions/{id}/turn`은 이제 평가 결과를 `feedback_scores`, `turn_technique`, `turn_client_state`, `supervisor_comment`, `alternative_utterance` normalized row로 교체 저장하고, 저장 실패는 503으로 표면화한다. 재평가 때 stale label/comment가 남지 않도록 같은 `turn_id`의 기존 evaluation row를 지운 뒤 재삽입하며, 이를 위해 normalized evaluation table의 delete RLS policy도 정적 SQL과 runtime ensure에 추가했다. DB-backed E2E는 실제 세션/턴을 만든 뒤 교수자 턴 재평가 응답의 technique 라벨이 `/review` learner turn에 hydrate되는지 확인한다. 검증: `py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_eval_routes.py app/test_evaluation_persistence.py -q` 29 passed, `npm run typecheck`, `PLAYWRIGHT_PORT=5246 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1 --grep "teacher turn reevaluation"` 1 passed, `npx playwright test --list` 현재 181 tests / 19 files.
최신 동기화 추가(2026-07-01): 음성 비언어 메타데이터도 mock/fixture 전용 검증에 머물던 구간을 좁혔다. `db.healthcheck()`, runtime table readiness, deploy preflight DB mode가 `app.turns.provider_events` 하나가 아니라 `audio_ref`/`silence_ms`/`speech_rate`/`barge_in`/`provider_events` 5개 컬럼 전체를 확인한다. dev/runtime bootstrap은 누락된 nullable 음성 컬럼도 보강한다. `session-persistence.spec.ts`에는 실제 DB 회기를 만들고 `/api/voice/ws` `stt_result` control 및 실제 Session 마이크 UI 생성 메타를 DB-backed `/review` 제한 `nonverbal` 칩까지 검증하는 `@single-run` E2E를 추가했다. 검증: `python -X utf8 -m pytest -p no:cacheprovider app/test_runtime_policy.py app/test_session_turn_persistence.py app/test_voice_ws.py app/test_voice_service.py -q` 77 passed, `PLAYWRIGHT_PORT=5199 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1` 5 passed, `python -X utf8 -m py_compile app/db.py app/auth_sessions.py app/test_runtime_policy.py ..\..\scripts\check-deploy-preflight.py`, `python -X utf8 scripts\check-deploy-preflight.py --skip-db --env-file infra\.env.example --allow-placeholder-secrets`.
최신 동기화 추가(2026-07-01): 실제 Session 마이크 UI 경로도 음성 비언어 메타를 생성하도록 보강했다. `AudioWorklet` 캡처는 PCM RMS/peak 기반으로 voice activity와 trailing silence를 추정하고 `audio_end``silence_ms`, `barge_in=false`, `provider_events`(`voice_activity`, `silence`)를 싣는다. `MediaRecorder` fallback은 raw PCM을 볼 수 없으므로 녹음 duration 기반 voice activity만 보낸다. 리뷰 read-model은 turn-level `silence_ms`가 이미 침묵 칩을 만들면 provider `silence` 칩을 중복 노출하지 않는다. `voice-success.spec.ts`는 실제 마이크 버튼 경로가 `audio_end` 메타를 보내는지 확인하고, `session-persistence.spec.ts`는 그 UI 생성 메타가 DB-backed `/review` `nonverbal`까지 이어지는지 확인한다. 검증: `npm run typecheck`, `python -X utf8 -m pytest -p no:cacheprovider app/test_runtime_policy.py app/test_session_turn_persistence.py app/test_voice_ws.py app/test_voice_service.py -q` 77 passed, `PLAYWRIGHT_PORT=5197 npx playwright test e2e/voice-success.spec.ts --project=chromium-single-run --workers=1` 2 passed, 최신 `PLAYWRIGHT_PORT=5202 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1` 6 passed, `npx playwright test --list` 현재 160 tests / 18 files.
최신 동기화 추가(2026-07-01): 음성 WebSocket에서 final transcript를 화면에 먼저 붙인 뒤 턴 저장이 실패하면, DB/review에는 없는 학습자 발화가 정상 transcript처럼 남을 수 있던 silent failure 후보를 수정했다. `/voice/ws`는 턴 저장 503을 `code=turn_persistence_unavailable`으로 구조화해 보내고, Session UI는 해당 임시 발화를 `저장 실패` 배지로 표시하며 `turnSeq`/턴 카운트/코칭 성공 흐름으로 확정하지 않는다. 대기 중 내담자 응답 말풍선은 제거하고, alert와 마이크 오류 상태로 사용자가 다시 시도할 수 있게 한다. 검증: `py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_voice_ws.py -q` 16 passed, `npm run typecheck`, `PLAYWRIGHT_PORT=5248 npx playwright test e2e/session-mvp.spec.ts --project=chromium-single-run --workers=1 --grep "pending voice transcript"` 1 passed, `npx playwright test --list` 현재 182 tests / 19 files.
최신 동기화 추가(2026-07-01): 본래 E2E goal을 계속 이어서 AI 튜터/평가 주변 silent error 후보를 추가로 닫았다. `GET /live-coach` 실패는 빈 이력으로 보이지 않고 이력 모달 alert로 표시한다. `POST /live-coach``persistence_source`를 반환하고, DB가 아닌 runtime 이력은 Session 카드와 이력 모달에서 임시 저장으로 드러낸다. 기존 pre/post 점수를 비우면 `저장된 값 기준`으로 오인하지 않고 invalid 입력과 저장 차단으로 표시한다. 교수자 대시보드 pending queue는 턴이 0개인 종료 세션을 제외해 `reviewReady=false` 세션이 검토 대기처럼 보이지 않게 했다. 검증: `C:\Users\encep\AppData\Local\Programs\Python\Python311\python.exe -m pytest apps/api/app/test_teacher_dashboard.py -q` 11 passed, `C:\Users\encep\AppData\Local\Programs\Python\Python311\python.exe -m pytest apps/api/app/test_session_turn_persistence.py -q -k "live_coach_turn_marks_runtime_persistence_source"` 1 passed, `npm run typecheck`, `PLAYWRIGHT_PORT=5251 npx playwright test e2e/session-mvp.spec.ts --project=chromium-desktop --workers=1 --grep "AI tutor|runtime AI tutor"` 4 passed, `PLAYWRIGHT_PORT=5252 npx playwright test e2e/session-review.spec.ts --project=chromium-desktop --workers=1 --grep "pre/post"` 1 passed, `npx playwright test --list` 현재 186 tests / 19 files. 이 변경은 로컬 작업트리 기준이며 아직 배포되지 않았다.
최신 동기화 추가(2026-07-01): 마무리 라운드에서는 새 구현 범위를 넓히지 않고, 긴 회기 평가가 늦게 완료될 때 리뷰 화면이 기존 짧은 polling limit 때문에 `평가 대기`처럼 고착될 수 있는 문제만 좁게 닫았다. `SessionReview`의 ready polling window를 약 90초로 늘리고, 10번째 review 응답에서야 ready가 되는 route-fixture E2E로 고정했다. 서브에이전트 감사에서 확인한 후속 후보는 별도 라운드로 남긴다: 교수자 대시보드의 stale-missing 평가 row 상태 불일치, 중복 `POST /end`가 ready 평가를 새 실패로 덮을 수 있는 경로, RAG/source-pack sync 503 skip 및 live coach RAG 실패 은폐. 검증: `npm run typecheck`, `PLAYWRIGHT_PORT=5253 npx playwright test e2e/session-review.spec.ts --project=chromium-desktop --workers=1 --grep "long-running session evaluation"` 1 passed, `npx playwright test --list` 현재 188 tests / 19 files. 이 변경은 로컬 작업트리 기준이며 아직 배포되지 않았다.
최신 동기화 추가(2026-07-02): 위 후속 후보 중 교수자 대시보드 stale-missing 평가 불일치, 중복 `/end` 재평가 덮어쓰기, AI 평가 미완료 상태의 수동 검토 완료, source-pack retrieval 503 skip을 닫았다. 교수자 대시보드는 상세 리뷰와 같은 missing session evaluation 합성 규칙을 써서 오래된 종료 회기를 `평가 실패`로 표시한다. 이미 종료된 세션의 `/end` 재호출은 background session evaluation을 다시 예약하지 않고, 평가가 `ready`가 아니면 `PUT /teacher/sessions/{id}/review-status status=closed`를 409로 거절해 큐에서 실패 회기가 숨지지 않게 한다. `kb-source-packs.spec.ts`는 evaluator retrieval 503을 skip하지 않고 실패로 본다. 검증: `C:\Users\encep\AppData\Local\Programs\Python\Python311\python.exe -X utf8 -m pytest -p no:cacheprovider apps/api/app/test_teacher_dashboard.py apps/api/app/test_session_turn_persistence.py -k "teacher_cannot_close_review_before_ai_session_evaluation_ready or teacher_can_mark_session_review_closed_with_note or end_session_does_not_reschedule_evaluation_for_already_ended_session or session_evaluation or stale_missing" -q` 8 passed, `npm run typecheck`, `PLAYWRIGHT_PORT=5254 npx playwright test e2e/session-review.spec.ts --project=chromium-desktop --workers=1 --grep "manual AI retry|long-running session evaluation|retry a failed AI session evaluation|retry fails"` 4 passed, `PLAYWRIGHT_PORT=5255 npx playwright test e2e/kb-source-packs.spec.ts --project=chromium-single-run --workers=1` 1 passed. 이 변경은 로컬 작업트리 기준이며 아직 배포되지 않았다.
최신 동기화 추가(2026-07-02): 마무리 라운드에서는 새 범위를 넓히지 않고 AI 튜터 quota stale UI와 음성 위기 종료 UI만 닫았다. 세션 UI의 로컬 `coachQuota.remaining=0`이 stale한 경우 서버 `GET /live-coach`로 quota를 재조회하고, 잔여 기회가 있으면 `POST /live-coach`를 계속 진행한다. 최신 quota exhaustion이나 재조회 실패는 이전 코칭 카드에 묻히지 않도록 `coachError`를 우선 표시한다. 음성 `reply.conversation_stopped`가 빈 텍스트로 끝나는 위기 경로는 TTS 종료 이벤트를 기다리지 않고 socket을 닫아 109 안전 게이트·입력 disabled·live-coach 미호출 상태를 유지한다. 검증: `npm run typecheck`, `PLAYWRIGHT_PORT=5259 npx playwright test e2e/session-mvp.spec.ts --project=chromium-desktop --workers=1 --grep "AI tutor|stale empty AI tutor quota|quota exhaustion|degraded AI tutor|voice conversation stop"` 7 passed, `npx playwright test --list` 현재 194 tests / 19 files. 이 변경은 로컬 작업트리 기준이며 아직 배포되지 않았다. 남은 후속 후보는 live coach LLM audit 실패가 현재 request 성공/실패 판정에 반영되지 않는 문제다.
최신 동기화 추가(2026-07-01): 회기 종료 후 background deep 평가가 실제 durable DB row로 저장되는지 fixture 없이 검증했다. `_schedule_session_evaluation()`은 생성 task를 반환하고 done callback에서 crash/cancel을 관찰하며, 평가 결과·timeout/error 저장이 durable store에 닿지 않으면 명시 로그를 남긴다. 교수자 명시 재평가 API는 평가 산출물이 생성됐지만 저장 실패하면 503으로 노출하고, `GET /eval/sessions/{id}/evaluation``status/error/durable`을 제공해 캐시 복원과 DB 저장 증거를 구분한다. `session-persistence.spec.ts`는 학습자 세션 생성→턴→종료→teacher 로그인→`/eval/.../evaluation` polling→교수자 리뷰 UI `평가 완료`까지 실제 DB/API/엔진으로 확인한다. 검증: `python -X utf8 -m pytest -p no:cacheprovider app/ -q` 309 passed, `npm run check:api-types`, `npm run typecheck`, `PLAYWRIGHT_PORT=5205 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1` 7 passed, `PLAYWRIGHT_PORT=5203 npx playwright test e2e/session-review.spec.ts --project=chromium-desktop --project=chromium-mobile --workers=1` 16 passed, `npx playwright test --list` 현재 162 tests / 18 files.
최신 동기화 추가(2026-07-01): 교수자 회기 리뷰 데스크톱에서 `overview`/`transcript`가 여러 CSS Grid row를 가로질러 오른쪽 검토 rail 높이를 나눠 먹으며 중앙에 큰 빈 row가 생기던 문제를 제거했다. 1181px 이상 교수자 리뷰는 main column + review rail 2열 wrapper로 분리하고, main 내부는 요약 전체폭, 차트/회기 흐름 2열, 축어록 전체폭으로 배치한다. 회귀 방지로 `layout-visual-gate.spec.ts`에 교수자 리뷰 폭별 dead vertical gap 검증을 추가했다. 검증: `npm run typecheck`, `npm run build`, `npx playwright test e2e/session-review.spec.ts --project=chromium-desktop --workers=1` 8 passed, `npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run --workers=1` 10 passed, `npx playwright test e2e/session-layout.spec.ts --project=chromium-desktop --project=chromium-mobile --workers=1` 8 passed.
최신 동기화 추가(2026-07-01): 교수자 학생 분석을 콘솔에서 분리했다. `GET /teacher/learners/{learner_id}/analysis`는 담당 범위 안의 특정 학습자 전체 회기를 오래된 순서로 반환하고, 제한 없는 회기별 추이 point, 라포·탐색·개입·정리 단계 분포, 검토 대기/완료 카운트를 함께 내려준다. `/teach`는 검토 큐·위기 알림·페르소나 검수·최근 회기 triage만 맡고, 좌측 메뉴의 `/teach/analysis`는 전체 학습자 검색 테이블 → 우측 행 펼침 요약 → `?learner=` 사용자별 상세 → 추이/전체 회기/단계 분석 탭 → 회기 리뷰 드릴다운을 맡는다. 검증: `python -X utf8 -m pytest -p no:cacheprovider app/test_teacher_dashboard.py -q` 6 passed, `npm run check:api-types`, `npm run typecheck`, `npm run build`, `PLAYWRIGHT_PORT=5215 npx playwright test e2e/teacher.spec.ts --project=chromium-single-run --workers=1` 9 passed, `PLAYWRIGHT_PORT=5217 npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run --workers=1` 12 passed, `npx playwright test --list` 현재 166 tests / 18 files.
최신 동기화 추가(2026-07-01): 교수자 학생 분석 상세를 다시 보강했다. 세션 read model은 `app_user.display_name`/`nickname`/`email`을 조인해 `learner_label`로 전달하고, 사용자 row가 없을 때만 축약 learner id로 폴백한다. `/teach/analysis?learner=` 상세의 기본 탭은 페르소나별 회기로 바뀌었고, 학습자가 연습한 페르소나별 행을 먼저 보여준 뒤 각 행 펼침에서 해당 페르소나의 회기 목록과 리뷰 진입을 제공한다. `추이`, `전체 회기`, `단계 분석` 탭은 보조 분석으로 유지한다. 5210 로컬 스택은 API 8000(dev) + web 5210으로 재기동해 실제 DB 응답에서 `Voice withdrawn`, `E2E Learner`, `노민지` 같은 표시명이 목록에 올라오는 것을 확인했고, 8001 prod API는 그대로 유지했다. 검증: `python -X utf8 -m pytest -p no:cacheprovider app/test_teacher_dashboard.py app/test_rbac_idor.py::TeacherAdminAuditTest::test_teacher_list_sessions_inserts_read_audit_log app/test_rbac_idor.py::TeacherAdminAuditTest::test_admin_load_session_inserts_read_audit_log -q` 8 passed, `npm run typecheck`, `npm run build`, `npm run check:api-types`, `PLAYWRIGHT_BASE_URL=http://localhost:5210 npx playwright test e2e/teacher.spec.ts --project=chromium-single-run --grep "shows selected learner analysis"` 1 passed, `PLAYWRIGHT_BASE_URL=http://localhost:5210 npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run --grep "professor (student analysis overview|learner detail analysis)"` 2 passed.
최신 동기화 추가(2026-07-01): 원천문서 갭 E2E 탐색을 이어서 H1/C3 silent failure 후보를 좁혔다. `session-persistence.spec.ts`는 Phase 3 pre/post 3척도 입력이 브라우저 리뷰 UI → `PUT /users/me/prepost-measures` → DB-backed `GET /users/me/prepost-measures` → 새로고침 후 UI 복원까지 실제 API/DB로 이어지는지 검증한다. C3 수동 재평가는 persona 기본 `theory_target`이 학습자가 선택한 session `theory_mode`를 덮어쓰던 우선순위 오류를 수정했고, turn 재평가도 `TurnContext.theory_mode`를 채우도록 회귀화했다. 서브에이전트 병렬 조사 결과 남은 큰 공백은 C1 워크시트 learner-save/teacher-review full browser+API+DB E2E, C2 safety_events→교수자 대시보드 E2E, H2 live-coach evidence modal/source-pack actual sync proof였다. 검증: `python -X utf8 -m pytest -p no:cacheprovider app/test_eval_routes.py app/test_notifications.py app/test_session_turn_persistence.py -q` 39 passed, `PLAYWRIGHT_PORT=5205 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1` 7 passed, `npx playwright test --list` 현재 162 tests / 18 files.
최신 동기화 추가(2026-07-01): 위 문단의 C1/C2 E2E 공백을 실제 브라우저/API/DB 검증으로 닫았다. C1은 학습자 사례개념화 워크시트 저장 → 교수자 검토 화면 read-only 복원 → `수정요청` 검수 메모 저장/재조회까지 `session-persistence.spec.ts`에 추가했다. C2는 `app.safety_events`가 runtime bootstrap/healthcheck에서 빠져 DB 정상처럼 보이던 readiness 공백과, `jsonb` codec 위에 이미 직렬화한 문자열을 넘겨 detail이 JSON string으로 저장되던 사일런트 오류를 수정했다. `record_safety_event()`는 RLS용 `ai_context=true`로 저장하고, 안전 알림 list 실패는 non-dev에서 fail-closed한다. 남은 큰 공백은 H2 live-coach evidence modal/source-pack actual sync proof다. 검증: `python -X utf8 -m pytest -p no:cacheprovider app/test_session_turn_persistence.py app/test_teacher_dashboard.py app/test_runtime_policy.py -q` 59 passed, `PLAYWRIGHT_PORT=5228 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1 --grep "case worksheet|crisis safety"` 2 passed, `npx playwright test --list` 현재 166 tests / 18 files.
최신 동기화 추가(2026-07-01): H2 live-coach evidence/source-pack actual sync 공백도 닫았다. RAG 회수 경로가 `kb.chunk.meta.source_version/citation/source_type``LiveCoachGrounding`으로 옮기지 않아 RAG-only 근거에서 버전·인용·source type이 조용히 사라질 수 있던 문제를 수정했고, `build_rag_index_payloads()`는 source title도 meta에 남긴다. `session-persistence.spec.ts`는 실제 DB/engine 코칭 응답의 0615 source pack `version/citation``근거 보기` 모달, DB-backed `/live-coach` 이력, reload 후 `C` 마커 history dialog까지 유지되는지 확인한다. 새 `kb-source-packs.spec.ts`는 unauth 401, learner 403, admin 202로 source pack sync 권한을 확인하고, 4개 licensed source id가 DB-backed evaluator RAG에 sync되는지와 source-scoped `/kb/eval-grounding` 회수가 가능하면 같은 source만 반환하는지 검증한다. 검증: `py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_session_turn_persistence.py -k "live_coach" -q` 5 passed, `py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_live_coach_sources.py -q` 9 passed, `npm run typecheck`, `PLAYWRIGHT_PORT=5229 npx playwright test e2e/session-mvp.spec.ts --project=chromium-desktop --workers=1 --grep "AI tutor coaching"` 1 passed, `PLAYWRIGHT_PORT=5229 npx playwright test e2e/session-review.spec.ts --project=chromium-desktop --workers=1 --grep "retry a failed AI session evaluation|retry fails"` 2 passed, `PLAYWRIGHT_PORT=5229 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1 --grep "AI tutor coaching history"` 1 passed, `PLAYWRIGHT_PORT=5229 npx playwright test e2e/kb-source-packs.spec.ts --project=chromium-single-run --workers=1` 1 passed, `npx playwright test --list` 현재 167 tests / 19 files.
최신 동기화 추가(2026-07-01): C3 이론모드도 mock-only 검증에서 한 단계 올렸다. 실제 Session UI에서 `CBT`를 선택해 회기를 시작하면 `POST /sessions` payload가 `theory_mode=cbt`를 싣고, DB-backed `GET /sessions/{id}` 상세도 같은 `theory_mode`를 복원하는지 `session-persistence.spec.ts`에 추가했다. 기존 backend 회귀는 재평가 경로가 persona 기본 `theory_target`보다 session `theory_mode`를 우선하는지 유지한다. 검증: `npm run typecheck`, `PLAYWRIGHT_PORT=5229 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1 --grep "selected CBT theory mode"` 1 passed, `npx playwright test --list` 현재 168 tests / 19 files.
최신 동기화 추가(2026-07-01): H4 동의 철회 경계도 실제 voice WebSocket 경로에서 보강했다. 기존 `/api/voice/ws?persona_code=...` 개발 세션 생성 경로는 온보딩/동의를 확인했지만, 이미 생성된 `session_id`로 바인딩하는 경로는 소유권/종료 여부만 보고 동의 철회 상태를 다시 보지 않았다. `_bind_session()` 앞단에 온보딩/동의 공통 게이트를 두어 기존 `session_id`와 개발 `persona_code` 경로 모두 같은 차단 규칙을 타게 했고, 브라우저 E2E는 학습자가 세션 생성 후 `DELETE /auth/consent`로 철회한 상태에서 같은 `session_id``/api/voice/ws`를 열면 `consent_required`로 닫히는지 검증한다. 검증: `py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_voice_ws.py -q` 15 passed, `PLAYWRIGHT_PORT=5231 npx playwright test e2e/voice.spec.ts --project=chromium-single-run --workers=1 --grep "consent withdrawal"` 1 passed, 당시 `npx playwright test --list` 169 tests / 19 files.
최신 동기화 추가(2026-07-01): X1 재귀학습 데이터셋 export의 사일런트 false-positive도 좁혔다. `export-recursive-dataset.py`는 이제 `u.consent_at IS NOT NULL`, non-empty `text_masked`, `'client' = ANY(t.visible_to)` 조건을 걸어 동의가 철회된 학습자와 evaluator-only 턴을 export 후보에서 제외한다. `supervisor_comments`는 raw `sc.text`를 선택하지 않고, service record builder도 comment 본문을 JSONL에서 제거한다. `dataset_export.validate_manifest_gate()`는 approved export에서 `include_withdrawn=false``recursive_learning_seed` consent scope를 직접 요구한다. `check-phase3-artifacts.py`는 approved뿐 아니라 dry-run JSONL도 required keys, schema, non-empty `text_masked`, array fields, privacy contract, row count, PII shape로 검증해 `{}` 한 줄이 통과하던 공백을 막는다. 남은 X1 후속은 실제 파일럿 roster/withdrawal log 대조, `participants_included/excluded` 일치, `min_completed_sessions` 정책, reviewer disposition/gold annotation round 적재다. 검증: `py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_dataset_export.py app/test_phase3_artifact_checker.py -q` 16 passed, focused py_compile 통과.
최신 동기화 추가(2026-07-01): H4 live coach prompt 경계에서 recent turns와 fast-loop evaluation dict가 서비스 내부 재마스킹 없이 그대로 engine user message에 들어갈 수 있던 누출 후보를 닫았다. 정상 route는 `text_masked`를 넘기지만 legacy hydrate/direct service call/test fixture가 raw 이름·기관·전화번호를 넣으면 `_messages()`가 그대로 직렬화하던 구조였다. `app/services/live_coach.py`는 이번 보강으로 이번 발화, 내담자 응답, recent turn text, evaluation 문자열 leaf/key를 모두 `guardrail.mask_pii()`로 통과시킨 뒤 프롬프트를 만든다. 이어서 `session_persistence`의 turn evaluation 저장 경계도 feedback rationale, supervisor comment text/intent_deviation, alternative utterance suggestion/rationale을 DB insert 전 마스킹하게 했고, legacy DB row에 `text_masked`가 비어 있을 때 raw `text`를 그대로 복원하던 fallback도 재마스킹한다. session-end deep evaluation은 write 생성, save 직전, legacy payload read-model 투영 시점 모두에서 문자열 leaf를 재마스킹해 summary/goodMoments/growthPoints/nextLine으로 raw PII가 노출되지 않게 했다. 같은 라운드에서 `test_evaluation_persistence.py``IsolatedAsyncioTestCase`로 전환해 async persistence 테스트가 실제 await되지 않던 false-positive를 제거했다. 검증: `py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_evaluation_persistence.py app/test_session_turn_persistence.py app/test_live_coach_privacy.py -q` 50 passed, `py_compile` 통과. 남은 H4 후속은 운영 ko recognizer/provider 선정과 guardian/legal evidence다.
분류: **B1 비차단 폴리시** · **B2 환경 제약(증거 생산 불가)** · **B3 소유자 결정** · **B4 외부 거버넌스**
> **B0. 원천문서 갭 분석 (2026-06-26 추가)** — 한신대 산학협력 원천문서 5종 정독으로 도출한 "부족한 부분"(critical 3 / high 4 / medium+ 6)은 **SSOT 대시보드** `docs/dev_dashboard.html` "원천문서 갭 분석" 섹션과 상세 `docs/ops/source-docs-gap-analysis-2026-06-26.md`에서 추적한다. C1 사례개념화 산출물은 저장형 워크시트, 외부 루브릭 scaffold, 교수자 수동 검수 상태 저장까지 4차 구조를 만들었고, C2 위기개입 프로토콜은 1차 구조, C3 이론모드는 2차 명시 선택 UI까지 만들었다. 콘텐츠 정의는 임상팀(구훈정·어유경) 소유라 코드는 구조를 선제 구축하되 임상 문안과 평가기준은 외부 정의로 받는다.
---
## B1. 비차단 코스메틱 폴리시
- [x] **공용 셸 사이드바 구분선 전체 높이** — (2026-06-26 처리) `.vg-nav border-right`(sticky·뷰포트 높이)를 제거하고, `.vg-shell__body` 컨테이너 배경 하어라인(`linear-gradient` 1px×100%, `--nav-cur`로 그리드·구분선 폭 동기화)으로 본문 그리드 전체 높이를 따라가는 세로 구분선을 그린다. 1024 축소폭은 `--nav-cur: var(--nav-w-collapsed)`로, 720 이하(상단 가로바 전환)는 `background-image:none`으로 정합. `components/shell/shell.css`. **검증: `npm run typecheck` PASS + `vite build` PASS.** 시각 회귀 게이트(gate 7/7 + 포커스 E2E + session-layout 8/8)는 web+api+DB 스택(Docker 미기동·secrets 부재)으로 이 환경 미실행 — 스택 가용 시 1회 시각 확인 권장.
- [x] **세션 종료 UX와 dark theme SSOT** — (2026-06-27 처리) 오발동과 모바일 조작 불안을 줄이기 위해 세션 하단의 드래그형 `SlideToEnd`를 명시 확인 다이얼로그로 교체했다. Topbar/Settings의 `vignette.theme` 읽기·저장·`data-theme` 적용은 `apps/web/src/lib/theme.ts` 단일 경로로 통합했고, 저장값이 없으면 dark 기본값을 부팅 전에 적용한다. API 기본 preference `system`은 Settings에서 light로 오해하지 않고 현재 초기 테마를 따른다. 회기 리뷰·설정 포함 주요 페이지의 흰 섹션 잔재도 dark 작업면으로 맞췄다. **검증: `npm run typecheck`, `npm run build`, `npx playwright test e2e/layout-visual-gate.spec.ts e2e/session-layout.spec.ts e2e/avatar-expression.spec.ts e2e/session-review.spec.ts e2e/settings.spec.ts --project=chromium-desktop --project=chromium-mobile --project=chromium-single-run --workers=1` 36 passed. 추가 시각 검사: 7개 주요 화면 x 3뷰포트 캡처 21장, horizontal overflow 0, 큰 흰 CSS 배경면 0.**
- [x] **P1 세션 아바타 PSD v2 정렬** — (2026-06-27 처리) 컨셉 보드 크롭 `p1-concept`를 기본값에서 내리고, 사용자 제공 `라투디 여캐_ver2.psd`에서 추출한 `seoyeon-live2d-psd-v2`를 P1 기본값으로 연결했다. `sad` 표정은 PSD의 울상 눈썹, 우는 입, 눈물 파츠를 별도로 합성하고, 데스크톱 스테이지는 3행 구조를 유지해 현재 상태 배지가 아바타와 겹치지 않게 했다. **검증: PSD 파츠 추출 53개, `rasterArtSet="seoyeon-live2d-psd-v2"`, sad 프리뷰에서 눈물/우는 입/울상 눈썹 확인.**
- [x] **SEO/GEO 기본 신호 + 회기 리뷰 URL 공유 카드** — (2026-06-27 처리) `index.html`에 canonical/description/Open Graph/Twitter Card/JSON-LD를 추가하고, `robots.txt`/`sitemap.xml`/`llms.txt`를 게시했다. 종료된 학습자 회기는 `POST /sessions/{id}/share`가 공개 토큰을 생성하고 `GET /share/session/{token}`이 서버 HTML로 요약·썸네일·JSON-LD를 내려 URL unfurl을 지원한다. `app.session_share_link`에는 토큰 해시와 sanitized preview payload만 저장하며 원문 축어록·학습자 식별자는 포함하지 않는다. 공유 페이지는 교수자 전달용이라 `noindex`를 유지한다. **검증: `pytest app/test_session_share.py app/test_session_turn_persistence.py -q` 21 passed, `python -m pytest app/ -q` 178 passed, `python -m pytest engine_gateway/ -q` 11 passed, `npm run check:api-types`, `npm run typecheck`, `npm run build`, `PLAYWRIGHT_PORT=5174 npm run e2e` 113 passed.** 배포 후 실제 카카오톡/Slack/메일 unfurl 1회 확인 권장.
- [x] **학습자·교수자·운영·페르소나 스튜디오 레이아웃 정렬** — (2026-06-28 처리) 작업형 화면에만 `AppShell wide` 폭 정책을 적용하고, 학습자 홈 회기 목록은 큐형 행으로 정리했다. 교수 콘솔은 검토 대기·위기 알림·페르소나 검수·페르소나 저작실 순서가 DOM과 화면 모두 일치하며, 운영 콘솔 사용자/티켓은 조밀한 운영 행으로 낮췄다. 페르소나 스튜디오는 SSOT 첨부/RAG 생성/항목 편집/검증 큐를 한 작업면에서 유지한다. 리포트와 대표 캡처: `docs/ops/layout-research-2026-06-28/dashboard-layout-alignment-report.md`, `docs/ops/layout-research-2026-06-28/final-gate-*.png`. **검증: `npm run typecheck`, `npm run build`, `npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run --workers=1` 7 passed, `npx playwright test e2e/session-layout.spec.ts --project=chromium-desktop --project=chromium-mobile --workers=1` 8 passed, `npx playwright test e2e/learner.spec.ts e2e/teacher.spec.ts e2e/admin.spec.ts --project=chromium-desktop --workers=1` 17 passed.**
- [x] **관리자 권한 위임 1차** — (2026-06-28 처리, 2026-06-29 보강) 역할(role)과 관리자 페이지 진입권(`app_user.admin_access`)을 분리했다. `AUTH_SUPER_ADMIN_EMAILS` 기본값은 `yunchan@twentyoz.kr`, `hoonjungkoo@hs.ac.kr`이며 슈퍼 관리자만 `/admin/users`에서 관리자 권한을 부여·회수한다. 실제 `admin` 역할 사용자는 학습자·교수자·관리자 라우트 전환이 가능하고, `/admin` 왼쪽 메뉴도 운영 홈/사용자/권한/티켓과 교수자·학습자 작업 공간을 함께 노출한다. 일반 학생/교수 계정은 `admin_access=true`여도 관리자 콘솔 진입권만 얻고 학습자·교수자 역할권은 추가되지 않는다. **검증: `python -B -m pytest -p no:cacheprovider app/test_auth_providers.py app/test_runtime_policy.py app/test_rbac_idor.py app/test_teacher_dashboard.py app/test_learner_dashboard.py -q` 70 passed, `python -B -m pytest -p no:cacheprovider app/test_auth_providers.py app/test_admin_ops.py app/test_runtime_policy.py -q` 64 passed, `npm run check:api-types`, `npm run typecheck`, `npm run build`, `npx playwright test e2e/admin.spec.ts --project=chromium-desktop --workers=1` 8 passed.**
- [x] **관리자·교수자 메일링 시스템 1차** — (2026-06-29 처리) 신규 외부 로그인 사용자가 `account_status=pending`이면 관리자 승인 메일 큐를 만들고, 회기 종료 후 평가 또는 error record가 저장되면 담당 교수자/관리자에게 회기 검토 메일 큐를 만든다. `app.notification_event``account_pending_approval:{user_id}`, `session_review_ready:{session_id}`, `admin_test_email:{uuid}` idempotency key로 중복 발송을 막고, `app.notification_delivery`는 수신자별 `queued/sending/sent/failed/skipped` 상태와 재시도 정보를 보관한다. 테스트 메일은 대량 오발송을 피하기 위해 `AUTH_SUPER_ADMIN_EMAILS` 대상에게만 큐잉한다. SMTP 설정은 `NOTIFICATION_EMAIL_PROVIDER=smtp`, `SMTP_*` env로 주입하며, 관리자 API `GET /admin/notifications`, `POST /admin/notifications/process`, `POST /admin/notifications/test``scripts/run-notification-worker.py`로 큐를 확인·처리한다. HTML 메일은 Vignette 종이 배경/세이지 CTA 톤을 inline style로 유지하고, 본문에는 축어록·평가 전문을 넣지 않는다. **검증: `python -m compileall apps\api\app\services\notifications.py apps\api\app\routes\admin.py apps\api\app\routes\sessions.py apps\api\app\auth_sessions.py apps\api\app\routes\users.py`, `python -B -m pytest -p no:cacheprovider app/test_notifications.py app/test_auth_providers.py app/test_admin_ops.py app/test_runtime_policy.py -q` 69 passed, `npm run generate:api-types`, `npm run check:api-types`, `npm run typecheck`, `npm run build`, `python scripts\check-deploy-preflight.py --env-file infra\.env.example --allow-placeholder-secrets --skip-db`, `python scripts\run-notification-worker.py --help`, `python -X utf8 scripts\check-dev-dashboard-ssot.py --json` PASS.**
- [x] **회기 아카이브 저장/복원 API** — (2026-06-28 처리) `/learn/history``보관됨`을 실제 학습자별 보기 상태로 연결했다. 종료 회기는 `POST /sessions/{id}/archive`/`restore`로 보관·복원하고, `app.session_archive_state`는 삭제가 아니라 `archived_at`/`updated_at`만 저장한다. `LearnerSessionSummary.archived`, `SessionArchiveResponse`, `LearnerDashboardOverview.archived_sessions`를 OpenAPI에 고정했고, 보관된 회기는 리뷰 대기 행동 큐에서 빠진다. 회기·턴·리뷰·공유 링크·연구/감사 증거는 삭제하지 않는다. **검증: `python -B -m pytest app/ -q` 178 passed, `python -B -m pytest engine_gateway/ -q` 11 passed, `npm run check:api-types`, `npm run typecheck`, `npm run build`, `npx playwright test e2e/learner.spec.ts --project=chromium-desktop --workers=1` 6 passed.**
- [x] **OpenAI TTS voice preset DB map 연결** — (2026-06-28 처리) `app.persona_voice_map`의 OpenAI row를 `/voice/ws` TTS voice 선택에 연결했다. 명시 query preset은 DB map보다 우선하고, 세션 바인딩은 `app.sessions.persona_id/persona_version`으로 voice map을 찾으며, dev persona 생성은 catalog persona id/version map을 사용한다. seed materializer는 기본 OpenAI voice map을 `ON CONFLICT DO NOTHING`으로 생성하고, dev 런타임 스키마 보강은 기존 DB의 `persona_voice_map` 누락도 복구한다. OpenAI가 아닌 provider row는 기존 persona-code fallback으로 안전하게 흡수한다. **검증: `pytest app/test_runtime_policy.py app/test_persona_review.py app/test_voice_service.py app/test_voice_ws.py -q` 75 passed, `pytest app/ -q` 178 passed, `pytest engine_gateway/ -q` 11 passed.**
- [x] **빈상태 컬럼 높이 여백** — (2026-06-28 처리) session-review 0건 상태는 1280px 이상에서 3컬럼 masonry를 쓰지 않고 빈 리뷰 전용 2컬럼 순차 레이아웃으로 전환한다. 가짜 기록·장식 콘텐츠를 넣지 않고, `EMPTY_REVIEW_SESSION_ID` fixture와 `session-review-empty` visual gate로 390/720/861/900/1024/1280/1440 전 폭을 검증한다. session-prestart와 learner-home은 기존 gate 대상에 남겨 함께 무회귀 확인했다. **검증: `npm run typecheck`, `npm run build`, `npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run --workers=1` 9 passed, `npx playwright test e2e/session-layout.spec.ts e2e/session-review.spec.ts --project=chromium-desktop --workers=1` 6 passed.**
- [x] **다크 UI v2 시안 실제 적용 라운드** — (2026-06-29 처리) `02/03/04-*-v2-dark-unified.png` 시안을 실제 React/CSS 화면으로 반영했다. P1 라이브 상담 세션은 내담자 무대 발화, 좌측 세션 진행, 우측 라이브 신호/안전 점검을 다크 훈련 화면으로 재구성했다. P2 회기 리뷰는 learner filled 상태를 3열 workbench(요약/축어록/평가 rail + 하단 워크시트)로 바꾸고 empty review는 sparse third column 없이 유지했다. P3 학습자 홈은 대시보드에서 연습 대상 레일을 제거하고, 진행 회기/리뷰 대기/최근 평가/라포 흐름을 상단 CTA 바로 올렸으며, 오늘 이어갈 회기·AI 코치·다음 연습 추천·최근 피드백을 하나의 `lh-work-cluster` 작업 묶음으로 재배치했다. 오른쪽 레일은 최근 기록, 리뷰 대기, 반복 대상만 담당한다. 생성 PNG는 앱에 직접 넣지 않고 레이아웃·밀도·톤 기준으로만 사용했다. 새 실제 렌더 캡처: `docs/design-verification/full-pages/02-learner-home-{desktop,tablet,mobile}.png`, `03-session-active-{desktop,tablet,mobile}.png`, `04-session-review-{desktop,tablet,mobile}.png`, `04-session-review-empty-{desktop,tablet,mobile}.png`. 실행 기록은 `docs/ops/dark-ui-refresh-implementation-plan-2026-06-28.md`에 고정했다. **최신 검증: `npm run typecheck`, `npm run build`, `npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run --workers=1` 9 passed.**
- [x] **세션 종료 평가 실패 복구 UX + 원인 저장** — (2026-07-01 처리) 긴 회기 종료 후 AI 평가가 45초 timeout으로 실패하면서 `error=""`만 저장되던 silent failure를 제거했다. 종료 실패는 리뷰 화면으로 넘어가지 않고 alert로 표시하며, 종료 후 리뷰는 `reviewReady=false` 상태에서 대기 polling으로 ready 전환을 기다린다. 교수자 검토 화면은 평가 실패/미준비 상태에서 `AI 평가 재시도` 버튼으로 `/eval/sessions/{id}/reevaluate`를 호출한다. 재시도 API는 실패 결과도 DB에 저장한 뒤 non-2xx로 노출한다. 재시도 실패 UI 유지와 저장된 평가 실패 read-model 노출도 회귀 테스트로 고정했다. background task crash 관찰과 저장 실패 표면화, `status/error/durable` 평가 조회, 실제 종료 후 durable DB 평가 row 생성 E2E까지 추가했다. 모바일 코칭 모드의 AI 튜터 카드 숨김 회귀도 함께 수정했다. **검증: `python -X utf8 -m pytest -p no:cacheprovider app/ -q` 309 passed, `npm run check:api-types`, `npm run typecheck`, `PLAYWRIGHT_PORT=5205 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1` 7 passed, `PLAYWRIGHT_PORT=5203 npx playwright test e2e/session-review.spec.ts --project=chromium-desktop --project=chromium-mobile --workers=1` 16 passed, 기존 `session-mvp`/layout focused gates 유지.**
- [x] **브라우저 SSE stream 저장 경로 회귀** — (2026-07-01 처리) `session-mvp` mock과 직접 `/turn` API 테스트 사이에 있던 실제 UI stream 저장 공백을 줄였다. `session-persistence.spec.ts`에 브라우저 Session UI가 실제 `POST /sessions/{id}/stream`을 호출하고, stream 완료 뒤 DB-backed `/review`에서 학습자/내담자 축어록이 반환되는지 검증하는 `@single-run` 테스트를 추가했다. 같은 파일에 AI 튜터 코칭 이력 저장/재로딩 회귀와 음성 비언어 메타 리뷰 회귀도 추가했다. **검증: `PLAYWRIGHT_PORT=5199 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1` 5 passed, `npm run typecheck`.**
- [x] **AI 튜터 코칭 이력 저장 실패 표면화** — (2026-07-01 처리) `POST /sessions/{id}/live-coach`가 코칭 제안 생성 후 `save_live_coach_event()` 실패를 `except/pass`로 묻어 카드만 보이고 이력/재로딩에는 사라질 수 있던 silent failure를 제거했다. 저장 계층의 prod fail-closed 503을 그대로 사용자/프론트에 노출하고, DB-backed `/live-coach` 이력과 학습자 발화 `C` 마커가 재로딩 뒤 유지되는지 E2E로 고정했다. **검증: `python -X utf8 -m pytest -p no:cacheprovider app/test_session_turn_persistence.py -k live_coach -q` 4 passed, `PLAYWRIGHT_PORT=5199 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1` 5 passed.**
- [x] **음성 비언어 메타 DB readiness + 리뷰 E2E** — (2026-07-01 처리) `/health`, runtime table readiness, deploy preflight가 `app.turns` 음성 메타 5개 컬럼 전체를 확인하게 했다. `session-persistence.spec.ts`는 DB-backed voice WS `stt_result`와 실제 Session 마이크 UI 생성 메타 → `/review` `nonverbal` 파생 칩까지 고정한다. 실제 provider 기반 한숨·울음 감지와 물리 마이크 장시간 WSS 실측은 계속 후속 gate다. **검증: backend focused 77 passed, `PLAYWRIGHT_PORT=5197 npx playwright test e2e/voice-success.spec.ts --project=chromium-single-run --workers=1` 2 passed, `PLAYWRIGHT_PORT=5199 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1` 5 passed, preflight skip-db PASS.**
- [x] **위기 안전 게이트 UI 덮어쓰기 방지** — (2026-07-01 처리) SSE/음성 위기 종료가 빈 client reply로 끝날 때 `내담자 응답 없음` 신호와 live-coach 호출이 안전 게이트를 덮던 경로를 막았다. `conversation_stopped`가 있으면 109 안전 자원, 입력 disabled, 위기 신호를 유지하고 코칭 요청을 보내지 않는다. **검증: `PLAYWRIGHT_PORT=5193 npx playwright test e2e/session-mvp.spec.ts --project=chromium-single-run --workers=1` 2 passed, `npm run typecheck`.**
- [x] **운영 티켓·헬스 샘플 이력 저장 모델 1차**`/admin` 운영 홈과 `/admin/tickets``app.admin_health_event`·`app.support_ticket` 실제 DB 모델에 연결됐다. `/users/support-tickets`는 인증 사용자의 문제·불만·장애 접수를 저장하고, `/admin/tickets`는 미해결 큐·우선순위·상태 변경을 처리한다. `/admin/uptime`은 상시 SLA가 아니라 관리자 콘솔이 관측한 헬스 샘플 정상률/중단 이력을 반환한다. 가짜 티켓 수·가짜 다운타임 숫자는 계속 금지. **검증: `python -m pytest app/test_admin_ops.py -q` 3 passed, `npm run generate:api-types`, `npm run typecheck`.**
- [x] **사용자별 운영 티켓 조회 UI** — (2026-06-28 처리) 학습자 Settings에 `지원 요청` 섹션을 추가해 본인이 접수한 티켓의 상태·카테고리·우선순위·담당 그룹·해결 메모를 읽기 전용으로 확인한다. `GET /users/support-tickets``reporter_id = current user` 기준으로만 조회하고, 본문과 신고자 이메일/이름은 응답·감사 검증에서 제외한다. **검증: `python -B -m pytest app/test_user_support_tickets.py app/test_admin_ops.py app/test_runtime_policy.py -q` 29 passed, `npm run generate:api-types`, `npm run check:api-types`, `npm run typecheck`, `npm run build`, `npx playwright test e2e/settings.spec.ts --project=chromium-desktop --workers=1` 7 passed, `npx playwright test e2e/admin.spec.ts --project=chromium-desktop --workers=1 --grep "operation tickets"` 1 passed.**
- [x] **상시 synthetic monitor/cron 샘플러 1차** — (2026-06-28 처리) `/admin/health`와 같은 헬스 계산을 `record_admin_health_sample()`로 공통화하고, 브라우저 세션 없이 `scripts/record-admin-health-sample.py``app.admin_health_event`에 서비스별 샘플을 append한다. Windows 예약 실행은 `scripts/install-health-sampler-task.ps1``VignetteAdminHealthSampler` Scheduled Task로 설치한다. 이 값은 SLA가 아니라 synthetic sampler가 관측한 최근 샘플 이력이다. **검증: `python -B -m py_compile apps/api/app/routes/admin.py apps/api/app/test_admin_ops.py scripts/record-admin-health-sample.py`, `python scripts/record-admin-health-sample.py --help`, `powershell -NoProfile -ExecutionPolicy Bypass -File scripts\install-health-sampler-task.ps1 -PrintOnly -IntervalMinutes 7`, `python -B -m pytest app/test_admin_ops.py app/test_runtime_policy.py app/test_user_support_tickets.py -q` 31 passed, 로컬 one-shot `recorded_count=5`, `status=ok`, `engine_mode=claude_cli`.**
- [x] **헬스 이벤트 retention/rollup 실행 경로 1차** — (2026-06-28 처리) raw `app.admin_health_event`와 일별 `app.admin_health_daily_rollup`을 분리했다. `scripts/maintain-admin-health-events.py``--rollup-days``--retention-days`를 명시해야 하며 기본은 dry-run이다. `--apply`는 rollup을 먼저 upsert한 뒤 retention window 밖 raw 샘플만 삭제하고, non-dev apply는 `--allow-non-dev-apply` 없이는 차단한다. `/admin/uptime` 요약은 raw 이벤트와 daily rollup을 중복 없이 합산하고, 상세 `events` 목록은 최근 raw 샘플만 유지한다. **검증: `python -B -m py_compile apps/api/app/db.py apps/api/app/auth_sessions.py apps/api/app/routes/admin.py apps/api/app/services/admin_health_maintenance.py scripts/maintain-admin-health-events.py`, `python scripts/maintain-admin-health-events.py --help`, `python -B -m pytest app/test_admin_health_maintenance.py app/test_admin_ops.py app/test_runtime_policy.py app/test_user_support_tickets.py -q` 36 passed, 로컬 dry-run `rollup_event_count=0`, `prunable_event_count=0`.**
- [x] **운영 티켓 필터·카테고리 큐·수동 처리 감사 1차** — (2026-06-28 처리) `/admin/tickets`가 status/category/priority/assigned_group/source_path/stale/search 필터를 서버 query로 처리하고, 운영 콘솔은 검색·상태·카테고리·우선순위·담당 그룹·정체 필터와 카테고리 quick queue를 제공한다. 티켓 접수와 관리자 수동 상태/우선순위/담당그룹/해결메모 변경은 `audit.audit_log`에 metadata-only(`support_ticket_create`, `support_ticket_update`)로 남기며, 제목/본문 전문은 감사 로그에 복제하지 않는다. **검증: `pytest app/test_admin_ops.py app/test_runtime_policy.py -q` 28 passed, `npm run generate:api-types`, `npm run check:api-types`, `npm run typecheck`.**
- [x] **운영 티켓 중복 저장·수동 연결 1차** — (2026-06-28 처리) `app.support_ticket`에 결정론적 `fingerprint``parent_ticket_id`를 추가했다. 사용자 티켓 생성은 fingerprint를 저장하지만 자동 병합·자동 담당그룹·자동 우선순위 변경은 하지 않는다. `/admin/tickets`는 같은 fingerprint의 중복 후보 수와 oldest parent 후보를 반환하고, 관리자는 기존 `PATCH /admin/tickets/{id}`로 parent 연결/해제를 수동 처리한다. self-parent와 parent cycle은 서버가 거부하며, 변경 감사는 `support_ticket_update` metadata-only로 남기고 subject/body 전문은 복제하지 않는다. Settings의 사용자별 티켓 조회에는 fingerprint/parent를 노출하지 않는다. **검증: `python -B -m py_compile app/auth_sessions.py app/routes/admin.py app/routes/users.py app/services/support_tickets.py`, `python -B -m pytest app/test_admin_health_maintenance.py app/test_admin_ops.py app/test_runtime_policy.py app/test_user_support_tickets.py -q` 38 passed, `npm run generate:api-types`, `npm run check:api-types`, `npm run typecheck`, `npm run build`, `npx playwright test e2e/admin.spec.ts --project=chromium-desktop --workers=1 --grep "operation tickets|duplicate"` 2 passed.**
- [ ] **운영 티켓 자동 분류·처리 후속** — Claude Recipe headless 자동 수정 후보, 관리자 승인 후 이슈 등록·PR/작업 스레드 생성, 처리 결과 audit trail 확장은 아직 설계/승인 필요. 담당 그룹 자동 배정·우선순위 escalation·raw/rollup 보존기간 같은 운영 정책은 B3 결정 항목에서 먼저 닫아야 한다. 자동 수정은 운영자 승인 전까지 실행하지 않는다.
- [x] **learner-home 로딩 스켈레톤 밀도(390)** — (2026-06-26 처리) '연습 대상' 로딩 스켈레톤을 빈 div 단일 셰이머에서 실제 카드 구조(마크 박스 + 이름/메타/요약 2줄 플레이스홀더)를 모사하도록 밀도 보강. `LearnerHome.tsx` (`.lh-skel__box`/`.lh-skel__line*`), reduced-motion 분기 동반 갱신. **검증: `npm run typecheck` PASS + `vite build` PASS.**
검증 기준: 변경 후 `npm run typecheck`, `e2e/layout-visual-gate.spec.ts`(현재 12/12), 레이아웃 포커스 E2E(54), `e2e/session-layout.spec.ts`(8/8) 무회귀. 이전 후속 라운드는 빈상태 레이아웃(`typecheck`, `build`, layout gate 9/9, session-layout+session-review desktop 6/6), 사용자별 티켓 조회 UI(backend 29, settings 7, admin ticket 1), synthetic health sampler(backend 31, one-shot recorded_count 5), health retention/rollup(backend 36, dry-run OK), 티켓 중복 저장·수동 연결(backend 38, API types/typecheck/build OK, admin tickets E2E 2), M2 route-level seed recall 주입(py_compile, M2 focused 68)을 확인했다. 최신 다크 UI v2 라운드는 `learner.spec.ts`+`session-review.spec.ts` 9 passed, `session-layout.spec.ts` 8 passed, `layout-visual-gate.spec.ts` 9 passed로 별도 기록한다.
---
## B2. 환경 제약 — 이 워크스테이션에서 증거 생산 불가
- [ ] **`vnet.18ka.net` 공개 live 전환** — 코드/스크립트는 `vnet.18ka.net`·`api-vnet.18ka.net`을 허용하도록 갱신 완료. Tailnet 주소 `https://alpaca-home.taile93291.ts.net`은 dev-login까지 검증됐고, public OAuth redirect가 로컬/Tailnet 세션으로 붙지 않는 혼선을 막기 위해 Tailnet Google 버튼과 직접 Google 시작 URL은 `local_oauth_unavailable`로 차단한다. 공개 vnet은 현재 (1) `vnet.18ka.net`/`api-vnet.18ka.net` DNS 부재, (2) cloudflared 현재 인증 컨텍스트가 `18ka.net` zone이 아니라 `chanpaca.net` zone에 route dns를 생성, (3) Google Console redirect URI `https://api-vnet.18ka.net/auth/callback` 등록 필요 때문에 live 완료 아님. 현재 prod API 8001 자체는 `db:true`, `engine:true`로 건강하다. 상세: `docs/ops/tailscale-vnet-runtime-2026-06-27.md`.
- [ ] **실배포 `infra/.env` owner-secret fill-in + real preflight** — compose 템플릿과 더미 env smoke는 통과했지만 실제 배포지는 소유자 비밀값이 필요하다. 필수값: `APP_DB_PASSWORD`, `OAUTH_GOOGLE_CLIENT_ID`, `OAUTH_GOOGLE_CLIENT_SECRET`, `OPENAI_API_KEY`, `SESSION_SECRET`, production-safe engine/voice flags. 현재 로컬 stray 값(`ENGINE_MODE=claude_p`, prod sample TTS flag 등)은 운영값으로 복사하면 안 된다. 실제 secret 주입 후 `scripts/check-deploy-preflight.py` DB 포함 모드와 배포지 health를 별도 증거로 닫는다.
- [ ] **공개 Google OAuth 실제 `/turn` proof** — 로그인 가능한 계정으로 `storageState` 캡처 후 `E2E_PUBLIC_AUTH=1` + `chromium-public-auth` 1회 통과 필요. (소유자 지시로 이번엔 보류.)
- [ ] **음성 캐스케이드 live** — (2026-06-29 갱신) 로컬 API에서 실제 OpenAI STT/TTS 경로는 live WS smoke로 통과했다(P2 세션, MP3 발화 → transcript 52자 → reply 105자 → TTS binary 66 chunks/266,496 bytes). Session UI는 Web Audio unlock 재생과 AudioWorklet-first PCM16 capture로 보강했고, backend는 `format=pcm` raw chunk를 STT 업로드 직전 WAV로 정규화한다. `/voice/ws`는 provider-neutral `stt_result` control을 받아 EOT ready/pending을 route에서 판단한다. synthetic mic E2E는 AudioWorklet 경로로 통과했다. 남은 범위는 실제 Deepgram interim/final WSS, 물리 마이크, 공개 WSS, 50분 양방향 장시간 실측이다.
- [ ] **claude_cli ↔ Messages API 폴백 동일성**`ANTHROPIC_API_KEY`가 있어야 Messages API 경로를 돌려 동일성 검증 가능. claude_cli 경로는 게이트웨이 probe로 live 실측 완료.
- [x] **저항엔진 openness 곡선 DB 실증** — (2026-06-28 처리) `scripts/smoke-resistance-openness-db.py`로 실제 API 경로(dev-login→onboarding→P1 세션 2개→각 5턴)를 실행하고 DB `app.session_state`/`app.turns`/`app.turn_client_state`를 직접 조회했다. 공감 5턴은 API curve `0.0→0.0→0.095→0.133→0.171`, DB final `stage=탐색`, `effective_openness=0.17100003361701965`; 조언점프 5턴은 API curve 전부 `0.0`, DB final `stage=라포`, `effective_openness=0.0`, `resistance=0.9580000042915344`. 증거: `docs/ops/resistance-openness-db-smoke-2026-06-28.json`, `docs/ops/resistance-openness-db-smoke-2026-06-28.md`. **검증: `python -B -m py_compile scripts/smoke-resistance-openness-db.py`, `pytest app/test_state_machine_resistance.py app/test_evaluation_persistence.py -q` 9 passed, live smoke PASS.**
- [ ] **재부팅 후 watchdog smoke**`watch-public-runtime.ps1` + Scheduled Task가 재부팅 후 엔진/API/터널을 복구하고 public `/turn`이 통과하는지 실측. 재부팅 불가로 미실행(parser/ check-only 경로는 확인됨). 2026-06-27 점검에서 아직 DNS가 없는 `api-vnet.18ka.net`은 기본 watchdog 검사에서 제외했고, 개통 후 `-AdditionalPublicHealthUrls`로 명시 추가하도록 runbook을 보강했다.
- [x] **상주 엔진풀 RSS 실측** — (2026-06-28 처리) gateway 세션 1개를 열고 `claude -p` 자식 프로세스 RSS를 Windows WorkingSet 기준으로 계측했다. `health.sessions=1` 상태에서 gateway `python.exe` PID 51592는 after-turn RSS 16.2MB, 상주 `claude.exe` PID 51688은 RSS 363.3MB/Private 628.8MB였다. Claude CLI plugin runtime `bun.exe` 자식 2개는 합계 RSS 178.6MB였다. 세션은 측정 직후 `DELETE /session/{id}`로 종료했다. 증거: `docs/ops/engine-rss-smoke-2026-06-28.md`.
---
## B3. 소유자(윤찬) 결정 필요 — 단독 결정 시 월권
> 결정되면 대시보드 `결정 필요` 탭과 관련 Phase 항목을 갱신한다.
- [x] **운영 티켓·가용성 정책 결정** — 결정됨(2026-06-30): 헬스 raw 90일·rollup 365일 보존, 우선순위 escalation은 운영자 수동 승인, SLA 미표기(관측 샘플 이력 유지), 담당 그룹 자동 배정 미도입. 후속: retention 기본값(raw 90/rollup 365) 적용·검증. escalation/그룹 자동화는 미도입으로 고정.
- [x] **평가설계 (H1: 실험/통제군·3척도·κ/ICC)** — 결정됨(2026-06-30): 평가자 신뢰도 κ≥0.70 & ICC≥0.75, 환각률 ≤3%, 독립표본 t-검정(α=0.05), 20명 실험/통제군 무작위 배정, 단회기 50분·3척도 pre-post 유지. 후속: KPI 측정계획·report threshold에 κ/ICC/환각률 반영. 실제 20명 파일럿 evidence·통계해석·공식 효과성 판정은 별도 GATE.
- [x] **s2s 2차 PoC 채택 여부** — 결정됨(2026-06-30): s2s 2차 PoC 유지. 캐스케이드 1차를 운영 기준으로 두고 `docs/decisions/voice-s2s-poc.md` 비교 PoC를 계속 연다. 최종 채택/폐기는 비교 리포트 이후. 후속: s2s 비교 PoC 구현·레이턴시/안전/감사 실측.
- [x] **서비스용 페르소나 음성 모델·권리 정책** — 결정됨(2026-06-30): 상업 라이선스가 명확한 대체 TTS provider 채택. Higgs v3 무참조 synthetic 샘플과 `VIGNETTE_VOICE_POC_SAMPLE_TTS=true` dev sample provider는 개발 플래그로만 유지하고, 실존 인물/성우 reference 사용 금지를 유지한다. 후속: 대체 TTS provider 선정·통합, dev/서비스 경로 분리 유지.
- [x] **fast-loop 외부 API 허용** — 결정됨(2026-06-30): 외부 폴백 허용. 로컬 상주 분류기 기본 + 외부 Haiku·Solar 폴백, 외부 전송 시 데이터주권·로그 보존·동의 가드 적용. 후속: 폴백 경로·전송 가드·관측 구현, 로컬 우선/외부 폴백 라우팅 정책 명시.
- [x] **재귀학습 fine-tuning 범위** — 결정됨(2026-06-30): fine-tuning 포함. few-shot 자동갱신에 더해 fine-tuning까지 범위에 두되, 데이터셋은 동의 범위·추가 축어록 수급 결정이 닫힌 뒤 구성한다. few-shot 자동갱신은 선행 가능.
- [x] **자유연습 기본값** — 결정됨: 기본 ON(+피드백 권장 배지+교수자 토글). 미동의·미성년 개인정보 gate만 차단한다. 근거: `docs/redteam/MASTERPLAN_REVISIONS.md` F-10 승인.
- [x] **추가 축어록 수급 규모·일정** — 결정됨(2026-06-30): 20건을 1개월 내 수급. 후속: 실제 수급·동의·정제는 임상/한신대 거버넌스 게이트와 연계.
- [x] **백엔드 언어 방향** — (2026-06-28 결정, 2026-06-29 보강) Node.js를 장기 교체 대상과 신규 분리 서비스 우선 스택으로 삼되, 현재 FastAPI 전면 재작성은 납품 일정 리스크라 보류한다. 전환은 계약 우선 strangler 방식으로 진행한다. 고정된 내부 경계는 `app/contracts/engine_gateway.py`의 gateway request/response/SSE 계약과 `gateway-default` sentinel, `structured_payload_from_response()`, `scripts/check-engine-gateway-contract.mjs` schema-driven Node conformance runner, `GatewayPromptParts`, `app/session_read_model.py`, `app/persona_read_model.py`, `app/persona_generation_contract.py`, `SessionEvaluationWrite.from_result()/from_error()`다. FastAPI route는 auth/RLS DB read/persistence, teacher/admin gate, RAG/LLM side effect, session lifecycle, provenance/error mapping을 계속 소유한다. 2026-06-29 후속으로 Python gateway의 `provider/model` 응답 메타 규칙을 helper로 모으고, Node runner가 `gateway-default` request sentinel과 resolved response model 분리를 검증하며, current-turn prompt split과 missing-user 400 경계를 테스트로 고정했다. 결정 기록: `docs/decisions/backend-node-transition.md`. **검증: `node scripts/check-engine-gateway-contract.mjs --json`, gateway contract 27 passed, persona contract+review 39 passed, backend focused 120 passed, `npm run check:api-types`, `npm run typecheck`.**
- [x] **Node 전환 경계 보강 — 페르소나 생성 계약 + 평가 저장 packet** — (2026-06-29 처리) `app/persona_generation_contract.py`가 페르소나 draft structured schema, prompt bundle id/version/hash, `GenerateResponse` payload extraction, generated draft coercion/default fallback을 소유한다. `SessionEvaluationWrite.from_result()/from_error()``app.session_evaluation` 저장 packet(`status/source/scope/stage/payload/error`) 생성을 소유한다. Node conformance runner는 stream event 목록과 payload 검증을 `engine_gateway_schema.v1.json``StreamPacket.oneOf`, payload `$ref`, `x-engine-gateway-sse-events`에서 읽는다. `routes/personas.py`는 auth, source 등록·조회, RAG evidence, engine 호출, provenance 조립, HTTP error mapping을 유지하고, `routes/sessions.py`/`routes/eval.py`는 factory로 named packet을 만들어 저장한다. **검증: `py -3.11 -X utf8 -B -m py_compile app\persona_generation_contract.py app\routes\personas.py app\persona_read_model.py app\test_persona_generation_contract.py app\test_persona_review.py engine_gateway\test_gateway_model.py app\session_persistence.py app\routes\sessions.py app\routes\eval.py app\services\evaluator.py app\services\live_coach.py`, `py -3.11 -X utf8 -B -m pytest -p no:cacheprovider app\test_persona_generation_contract.py app\test_persona_review.py -q` 39 passed, backend focused 120 passed, `node scripts\check-engine-gateway-contract.mjs --json`, `npm run check:api-types`, `npm run typecheck`.**
---
## B4. 외부 거버넌스 — 한신대/데이터 steward 서면 증거
- [ ] **한신대 데이터/SSO 거버넌스 게이트** — SSO claim mapping, 추가 축어록 수급, 미성년 원본 활용동의, 개인정보 처리방침. 로컬 문서 골격 `docs/ops/hanshin-data-governance-gate.md` 준비됨, 외부 서면 증거 필요. (소유자 지시로 이번엔 보류.)
- [ ] **L1 스택 정합성·저작권 등재 문서 반영** — 백엔드 방향은 Node.js 교체 가능성 우선으로 결정됐지만, 신청서/저작권 등재 문서에는 현재 FastAPI 유지 사유와 계약 우선 전환 계획을 반영해야 한다. 로컬 결정 기록은 `docs/decisions/backend-node-transition.md`; 외부 제출/등재 증거가 필요하다.
---
## Phase 3 파일럿 게이트 (실참여자 필요)
- [ ] 20명 교육용 파일럿 운영 / 효과성·KPI 측정(SUS·자기효능감·κ/ICC·환각률) / 재귀학습 데이터셋 approved 산출 / 개인정보·동의 감사. 문서/checker와 dry-run exporter는 준비됨(`docs/phase3/*`, `scripts/check-phase3-artifacts.py`, `scripts/export-recursive-dataset.py`). 2026-06-27 checker 강화로 CSV enum, KPI metric required fields, approved export의 PII pass·κ/ICC·withdrawn exclusion·consent scope·file sha256 false-positive 차단을 추가했다. 2026-06-28에는 3척도 pre/post aggregate evidence 원장 `app.learner_prepost_measure`, 학습자 본인용 `GET/PUT /users/me/prepost-measures`, `SessionReview` 입력 UI를 추가했고, `app.services.phase3_kpi_export`/`scripts/export-phase3-kpi.py``prepost_measures.csv``kpi_report.json` scaffold를 생성한다. participant id는 가명화하고 3척도 paired normalized delta, complete/missing pair를 계산한다. `phase3_kpi_contract.py`는 KPI metric 이름·필수키·`computed_prepost`/`design_pending` status 값을 공유해 exporter/checker/test drift를 줄이고, checker는 status unknown 값을 error로 처리한다. 공식 문항 확정, 통계 검정, 실험/통제군 배정, 실제 approved evidence는 파일럿·steward/legal 검수·IAA 통과 시점. **검증: `py -3.11 -X utf8 -m py_compile apps\api\app\services\phase3_kpi_contract.py apps\api\app\services\phase3_kpi_export.py apps\api\app\test_phase3_kpi_export.py apps\api\app\test_phase3_artifact_checker.py scripts\check-phase3-artifacts.py scripts\export-phase3-kpi.py`, `py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_phase3_kpi_export.py app/test_phase3_artifact_checker.py -q` 10 passed, `py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_user_support_tickets.py app/test_runtime_policy.py app/test_admin_ops.py app/test_phase3_kpi_export.py app/test_phase3_artifact_checker.py -q` 46 passed, `py -3.11 -X utf8 scripts\export-phase3-kpi.py --help`, `py -3.11 -X utf8 scripts\check-phase3-artifacts.py --help`.**
---
## 이번 세션에 닫은 것(참고)
- 레이아웃 시각 수용: 기본 게이트 7/7 + 적대적 재검수 7/7 accept에 더해 빈 회기리뷰, 교수자 리뷰, 학생 분석 목록/상세 gate를 추가한 현재 12/12 — DONE.
- live 운영 증거: engine config 운영값, 상주 엔진풀 probe(TTFT/cost/세션재사용), 상주 엔진풀 RSS 실측, Postgres RLS/audit smoke 5 checks PASS, turn cost telemetry(app.turns 13행) — DONE.
- P1 서연 음성 아트 PoC: Higgs v3 무참조 synthetic seed + 5개 정서/속도 변주(mp3/wav) 생성. 실존 reference voice 미사용. `/voice/ws`에는 dev-only sample TTS provider로 연결했고 live text_turn smoke에서 `p1-sample-poc` binary 20 chunks/78,573 bytes 수신 확인. 이 provider는 TTS override만 담당한다(마이크/STT는 OpenAI 키 필요). 프로덕션 탑재는 라이선스/권리 결정 전 금지.
- Session.tsx 음성 guard/view-model P2 리팩터 + capture/EOT 계약 보강: `isVoiceStatusBusy()``sessionVoiceStatusView()`가 마이크 busy/disabled, aria label, transcript/response/status label, 텍스트 입력 차단 계산을 소유한다. Session mic capture는 `AudioWorklet`-first PCM16 + `MediaRecorder` fallback이고, `/voice/ws``stt_result` control로 EOT ready/pending을 route에서 판단한다. 검증은 web typecheck/build, `session-mvp`, `voice-success` synthetic UI, `session-layout`, `test_voice_ws.py/test_voice_service.py` 범위만 인정한다. 실제 Deepgram WSS, 물리 마이크·공개 WSS·50분 장시간 실측은 B2 음성 캐스케이드 live 항목에 계속 남긴다.
- 부수: RLS smoke 스크립트 cleanup RLS 컨텍스트 버그 수정, `vignette_app`(NOBYPASSRLS) dev 역할 프로비저닝.
- 평가 SSOP 2차: `TurnRecord.evaluation``app.feedback_scores`/`turn_technique`/`turn_client_state`/`supervisor_comment`/`alternative_utterance`로 정규화 적재·복원하고, submit/stream/voice 저장 경로를 `app.turn_runtime`으로 공통화했다. 원시 평가 테이블은 evaluator/admin RLS로 제한하고 서버가 evaluator 컨텍스트로 적재한다. `app.turns` insert는 `RETURNING id` 확인으로 silent success 제거. **검증: `pytest app/test_evaluation_persistence.py -q` 6 passed, 관련 회귀 45 passed, `pytest app/` 119 passed, `pytest engine_gateway/` 7 passed.**
- 라이브 코칭 1차 + 이력 UI/RAG sync + 학습자 리뷰 3열 workbench: `app/services/live_coach.py`, `data/kb/live_coaching_workbook_0615.json`, `data/kb/live_coaching_sources/*.json`, `POST /sessions/{id}/live-coach`, `GET /sessions/{id}/live-coach`, `POST /kb/live-coach/source-packs/sync`, `app.live_coach_events`를 추가해 0615 사례개념화 워크북·DSM·공식 지침 요약 기반 코칭을 구조화했다. 세션 UI는 코칭 모드에서 AI 코치 아바타 말풍선, 근거 모달, 학습자 발화 우측 `C` 마커, 채팅 위 스크롤 이력 오버레이를 제공한다. 저장 payload는 PII 마스킹 excerpt와 코칭 구조화 JSON으로 제한한다. 공식 자료는 허가된 source pack의 version/citation/summary로만 코칭에 넣고 장문 원문·공식 문항 재현은 하지 않는다. 같은 source pack은 `app.services.source_pack_sync`로 evaluator 전용 RAG에 증분 색인하며, active `content_hash`가 바뀌면 `kb.document.version`을 최신+1로 올려 UNIQUE 충돌을 피한다. 2026-06-28 refactor-governance P4에서는 live turn의 프로세스 로컬 source pack snapshot은 유지하되 관리자 sync/CLI가 `refresh=True`로 캐시를 비우고 repo 파일을 다시 읽게 해 stale `content_hash` 비교를 막았다. 학습자 `SessionReview` 데스크톱은 요약/흐름, 축어록, 평가 rail의 3열 workbench와 하단 워크시트로 재배치했고, empty review는 2열 이하로 유지한다. 2026-06-29에는 워크시트/pre-post/교수자 메모 입력에 name/autocomplete/aria-label과 공유 focus token을 보강하고, 발화 이동 버튼 aria-label을 추가했다. **검증: `python -m compileall app -q`, 과거 `pytest app/test_live_coach_sources.py app/test_orchestrator_masking.py app/test_session_turn_persistence.py -q` 27 passed, source sync `py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_live_coach_sources.py -q` 9 passed, H2 related `py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_live_coach_sources.py app/test_orchestrator_masking.py app/test_session_turn_persistence.py -q` 44 passed, `py -3.11 -X utf8 scripts\sync-persona-sources.py --help`, `npm run generate:api-types`, `npm run check:api-types`, `npm run typecheck`, `npm run build`, `npm run e2e -- e2e/layout-visual-gate.spec.ts e2e/session-layout.spec.ts` 15 passed. 3열 workbench 검증: `npm run typecheck`, `npm run build`, `npx playwright test e2e/session-review.spec.ts --project=chromium-desktop --workers=1` 3 passed, `npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run --workers=1` 9 passed, `04-session-review-desktop.png` 직접 확인. 로컬 DB smoke: sync 4 sources/15 chunks embedded, 재호출 skipped_unchanged 4, `official_suicide_risk_guidelines` source_id 필터 검색 성공.**
- 계약 SSOT 7차: FastAPI OpenAPI를 `apps/web/src/lib/api.gen.ts`로 생성하는 `npm run generate:api-types`와 stale 체크용 `npm run check:api-types`를 유지한다. `TurnResponse`/`SessionEndResponse`에 이어 `MeResponse`, `AuthConfigResponse`, `ConsentResponse`, `PersonaSummary`, `PersonaReviewSummary`, admin/user/engine DTO, review leaf DTO, `CrisisResourceResponse`, teacher safety/growth leaf DTO, `LearnerSessionsResponse`, `LearnerDashboardResponse`, `SessionReviewResponse`, worksheet aggregate, `TeacherDashboardResponse`, `TeacherSessionSummary`, `TeacherLearnerGrowth`, `SessionStartResponse`, `SessionDetailResponse`, `SessionDetailTurn`를 생성 타입 alias로 전환했다. 세션 응답 stage는 `StageLabel` enum으로 OpenAPI에 고정했고, 69차에서는 `ReviewPhaseSegment.key/label`, `SessionReviewResponse.reachedPhase`, `EvaluationSummary.stage`, 평가 트리거 응답 stage, `TeacherSessionSummary.stage`, `TeacherGrowthPoint.stage`도 union/null 계약으로 축소했다. 71차에서는 공개 공유 응답 `PublicSessionShareResponse.reachedPhase`와 live-coach 이력 `LiveCoachEvent.stage``StageLabel | null` 계약으로 축소했다. 72차에서는 `app/stage_contract.py``StageLabel`, `ReviewPhaseKey`, legacy stage code 정규화, unknown→null 판단, 리뷰 phase key 매핑을 소유하게 해 `turn_runtime.py`, `session_read_model.py`, `routes/eval.py`의 중복 stage 규칙을 제거했다. generated optional/default 차이는 UI form state, notification default helper, 배열 렌더링 fallback, live-coach `단계 미상` 표시로 흡수한다. `.github/workflows/api-contract.yml`은 PR/master push에서 `npm run check:api-types` drift를 막는다. **검증: `py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_evaluation_persistence.py app/test_evaluator_model_routing.py app/test_teacher_dashboard.py app/test_rbac_idor.py app/test_session_turn_persistence.py -q` 50 passed, `py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_session_share.py app/test_live_coach_sources.py app/test_session_turn_persistence.py app/test_orchestrator_masking.py -q` 43 passed, `py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_stage_contract.py app/test_session_share.py app/test_session_turn_persistence.py app/test_evaluation_persistence.py app/test_teacher_dashboard.py -q` 42 passed, `py -3.11 -X utf8 -m py_compile apps/api/app/stage_contract.py apps/api/app/turn_runtime.py apps/api/app/session_read_model.py apps/api/app/routes/eval.py apps/api/app/routes/sessions.py apps/api/app/routes/share.py apps/api/app/services/live_coach.py`, `npm run check:api-types`, `npm run typecheck`, `npm run build`, 과거 `learner.spec.ts + session-review.spec.ts + teacher.spec.ts + layout-visual-gate.spec.ts` 32 passed.**
- 위기게이트 109 1차: 실제 자해·자살 신호는 LLM/엔진 호출 전 중단하고 `crisis_resource(109)``conversation_stopped`를 REST/SSE/voice 응답에 싣는다. `app.safety_events` detail 적재와 교수자 대시보드 안전 알림 큐까지 연결했다. 안전 이벤트 DB insert payload도 회귀화했다. 임상 문안, 실시간 push/메일, 지역 자원 확장은 후속. **검증: `pytest app/test_session_turn_persistence.py app/test_voice_ws.py -q` 20 passed, `pytest app/test_teacher_dashboard.py app/test_session_turn_persistence.py app/test_voice_ws.py -q` 21 passed.**
- 교수자 성장 추적/리뷰 상세 진입 1차: `teacher_dashboard`가 정규화된 턴 평가를 기반으로 학습자별 적절성·라포·기술 사용 추이를 집계하고, 교수 콘솔에 최근 회기/항목별 성장 카드와 안전 알림 큐를 함께 표시한다. 종료 회기 리뷰 대기 행과 최근 회기 행은 `/teach/session/:sessionId/review`로 이어지고, 교수자는 회기 리뷰와 사례개념화 워크시트를 읽기 전용으로 검토한다. 학습자 워크시트 저장은 계속 learner 전용이다. **검증: `pytest app/test_teacher_dashboard.py app/test_session_turn_persistence.py app/test_voice_ws.py -q` 21 passed, `pytest app/test_rbac_idor.py app/test_teacher_dashboard.py -q` 9 passed, `npm run check:api-types`, `npm run typecheck`, `npm run build`, `npx playwright test e2e/teacher.spec.ts --project=chromium-single-run --workers=1` 4 passed.**
- 교수자 검토 상태 1차: `app.session_review_status`에 교수자 회기 검토 상태를 저장하고, teacher dashboard 응답에 `review_status/review_note/reviewed_at`를 포함한다. `PUT /teacher/sessions/{session_id}/review-status`로 교수자 메모 저장과 검토 완료 처리를 수행하며, 완료된 회기는 pending queue에서 제외한다. 교수자/관리자의 `/sessions/{id}/review` 읽기 허용은 유지하고, 학습자 워크시트 저장은 learner 전용으로 계속 제한한다. 교수 콘솔 검토 큐는 숨김 처리하지 않고 상단 triage로 노출하며, 학습자 리뷰 화면에는 교수자 전용 grid 빈칸을 예약하지 않는다. **검증: `pytest app/test_teacher_dashboard.py app/test_rbac_idor.py app/test_learner_dashboard.py -q` 12 passed, `npm run check:api-types`, `npm run typecheck`, `npm run build`, `teacher.spec.ts` 4 passed, `session-review.spec.ts` 2 passed, `layout-visual-gate.spec.ts` 7 passed.**
- 학습자 개인화 대시보드 1차: 교수자 성장 집계와 중복되던 점수/라포/기법 계산을 `app.services.session_metrics`로 공용화하고, `GET /sessions/dashboard`가 본인 세션 기반 `overview/growth/persona_progress/achievements/recent_feedback`를 반환한다. 학습자 홈은 누적 회기, 리뷰 대기, 최근 평가, 라포 흐름, 페르소나별 진행, 최근 피드백, 마지막 세션 리캡을 실제 서버 데이터로 표시한다. 성취는 공식 등급/수료가 아니라 실제 연습 milestone만 사용한다. 2026-06-29 refactor-governance 패스에서 `session_metrics.turn_technique_label()`이 DB 재수화 계약의 `label_ko`를 우선하고 기존 `label/name/id/code` fallback을 유지하도록 고정했다. **검증: `py -3.11 -X utf8 -B -m pytest -p no:cacheprovider app/test_evaluation_persistence.py app/test_evaluator_model_routing.py app/test_session_turn_persistence.py app/test_rbac_idor.py app/test_teacher_dashboard.py app/test_learner_dashboard.py -q` 59 passed, 기존 `npm run check:api-types`, `npm run typecheck`, `npm run build`, `learner.spec.ts` 6 passed, `session-review.spec.ts` 2 passed, `teacher.spec.ts` 4 passed, `layout-visual-gate.spec.ts` 7 passed, `session-layout.spec.ts` 4 passed.**
- 페르소나 스튜디오/RAG 저작 10차: 교수 콘솔의 JSON 패널을 분리해 teacher/admin 전용 `/teach/personas` 3열 스튜디오(좌: 저작 흐름·카탈로그·검수 큐, 중앙: RAG 자료 등록·항목형 편집 탭, 우: 검증·RAG 근거·검수 결정)로 옮겼다. 2026-06-27 UI 정리에서 중복 네이티브 파일 입력을 커스텀 SSOT 첨부 드롭존으로 교체하고, 저작 스테퍼·탭별 작성 가이드·검증 dot 정렬을 보강했다. 2026-06-28 항목형 목록 UI 정리에서 자동사고, 회기 시나리오, 말투 filler/verbal tic/nonverbal cue, 역린·금기 응답·금기어를 행 추가/삭제 UI로 전환했고, 저장 payload는 기존 배열 schema를 유지하며 빈 항목만 저장 직전에 제거한다. 프롬프트 탭은 raw JSON textarea 대신 L1 카드·인적 범주·임상 배경·말투·수치 파라미터·역린·회기 시나리오·추가 계약 섹션으로 같은 draft 데이터를 라벨형 검토 UI에 표시한다. 레이아웃 근거와 생성 시안은 `docs/ops/layout-research-2026-06-27/persona-dashboard-layout-guideline.md`에 묶었다. `POST /personas/sources`는 첨부/붙여넣기 자료를 PII 마스킹 후 raw 원문 hash-only 증거를 `kb.raw_source_artifact`에 따로 기록하고, sanitized 파생본만 `kb.source/document/chunk`에 evaluator 전용(`visible_to=['evaluator']`, `sensitivity=2`) 근거 문서로 등록한다. `rag.index_document()``sensitivity=3` 또는 raw marker chunk를 DB 접근 전에 차단해 raw 원문이 `kb.chunk`/embedding/FTS에 들어가지 않게 한다. `POST /personas/drafts/generate``source_id` 기반 RAG 검색 결과만 생성 프롬프트에 넘긴다. 생성 응답과 draft `source_provenance`에는 source id, doc/content hash, chunk id, prompt bundle id/version/hash(`persona-draft-rag@2026-06-28.1`)를 남긴다. P1~P7 시스템 페르소나는 DB 저작 카탈로그의 초기 부트스트랩으로 승격했고, `materialize_seed_personas()`는 누락분만 insert해서 교수 편집본이나 `archived` 보관본을 덮어쓰지 않는다. `scripts/materialize-persona-seeds.py`는 같은 seed/version manifest를 dry-run 기본으로 보고하며, `--apply`일 때만 DB pool을 초기화한 뒤 기존 idempotent materializer를 호출한다. `scripts/sync-persona-sources.py``app.services.source_pack_sync`는 repo-managed source pack의 active `content_hash`를 DB에서 비교하고, 변경 시 `kb.document.version`을 최신+1로 색인한다. 교수자는 공개 목록에서 승인본을 다음 버전 draft로 복제해 수정하고, 불필요한 페르소나는 기존 회기 FK 보존을 위해 같은 code family 전체를 `archived`로 보관 처리한다. `app/persona_read_model.py`는 catalog/review/draft/source/evidence DTO와 mapper를 route에서 분리해 schema 이름과 `source/degraded` fail-closed 경계를 유지한다. 암호화 blob/vault 기반 원문 실저장, 임상팀 최종 검수/서면 evidence, 루브릭·이론 콘텐츠 외부화는 후속. **검증: `python -B -m py_compile app/services/rag.py app/routes/personas.py app/routes/kb.py app/test_persona_review.py app/test_live_coach_sources.py`, `python -B -m pytest -p no:cacheprovider app/test_persona_review.py app/test_live_coach_sources.py -q` 38 passed, `python -B -m py_compile app/persona_read_model.py app/routes/personas.py`, `python -B -m pytest -p no:cacheprovider app/test_persona_review.py -q` 33 passed, `python -B -m pytest -p no:cacheprovider app/test_persona_review.py app/test_session_turn_persistence.py -q` 55 passed, `npm run check:api-types`, `npm run typecheck`, `npm run build`, `py -3.11 scripts\materialize-persona-seeds.py --json` dry-run `manifest_count=7`, `py -3.11 scripts\sync-persona-sources.py --help`, `npx playwright test e2e/teacher.spec.ts --project=chromium-single-run --workers=1` 8 passed, 프롬프트 탭 포함 `layout-visual-gate.spec.ts` 9 passed, `session-layout.spec.ts` desktop/mobile 8 passed.**
- 페르소나 생성 계약 11차 + 평가 저장 packet: `app/persona_generation_contract.py`가 draft structured schema, prompt bundle, legacy/structured `GenerateResponse` payload extraction, generated draft defaults/coercion을 소유한다. 기존 `routes/personas.py`는 teacher/admin gate, RAG evidence 수집, engine request, provenance 조립, DB/HTTP error mapping을 계속 담당한다. `SessionEvaluationWrite.from_result()/from_error()`는 session evaluation 저장 packet 생성을 소유해 route의 long keyword bag을 제거했다. Node conformance runner는 `StreamPacket.oneOf`, payload `$ref`, `gateway-default` request sentinel과 resolved response model 분리를 읽어 event/payload/default-routing 검증 중복을 줄였고, `GatewayPromptParts`는 current-turn prompt split 경계를 명명했다. **검증: persona contract+review 39 passed, backend focused 120 passed, Node conformance OK, gateway contract 27 passed, `npm run check:api-types`, `npm run typecheck`.**
- M1 비언어 이벤트 4차: voice learner turn에 이미 저장하던 `audio_ref`/`silence_ms`/`speech_rate`/`barge_in`을 리뷰 API `nonverbal` 이벤트로 노출하고, 회기 리뷰 축어록에 침묵·발화 속도·끼어듦·음성 입력 칩을 표시했다. 2차에서는 `app.turns.provider_events JSONB``TurnRecord.provider_events`를 추가해 WebSocket control/STT provider 이벤트를 allowlist·size limit 후 보존했고, 3차에서는 저장 전 sanitizer에서 내부 taxonomy `event_type`/`category`를 붙인다. 4차에서는 인증된 회기 리뷰에 한숨·울음·웃음·호흡·운율·배경소음 계열만 한글 label/detail 칩으로 파생 노출한다. 이번 보강에서는 `/health`·runtime readiness·deploy preflight가 음성 메타 5개 컬럼 전체를 확인하고, 실제 Session 마이크 UI가 `audio_end`에 browser voice activity/trailing silence 메타를 보내며, DB-backed voice WS/UI E2E가 `/review` `nonverbal` 칩까지 이어지는지 확인한다. raw transcript/text/provider/source/raw type은 응답에서 제외하고, 공개 공유 카드에는 축어록과 provider raw를 싣지 않는다. 실제 provider 기반 한숨·울음 감지, 역량 지표화 정책, live 마이크/WSS 장시간 실측은 후속. **검증: `python -X utf8 -m pytest -p no:cacheprovider app/test_runtime_policy.py app/test_session_turn_persistence.py app/test_voice_ws.py app/test_voice_service.py -q` 77 passed, `PLAYWRIGHT_PORT=5197 npx playwright test e2e/voice-success.spec.ts --project=chromium-single-run --workers=1` 2 passed, `PLAYWRIGHT_PORT=5199 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1` 5 passed, `npm run typecheck`, preflight skip-db PASS.**
- M2 다회기 케이스 아크 11차: DB 세션 생성 시 `(persona_id, learner_id)` 기준 `case_profile`을 upsert하고, `session_no`를 트랜잭션 안에서 원자 증가시키며, `InProcSession.case_id`가 매회 새 `runtime_case_id`가 아니라 안정 `case_id`를 가리키게 수정했다. 시작/submit/stream/voice 경로는 case recall cache를 사용한다. 세션 종료 시 마스킹 축어록 기반 fallback `session_summary.digest``case_profile.case_digest`, `rapport_trajectory`, `alliance_level`을 갱신하고, 다음 회기 seed recall에서 `case_digest`·직전 `session_summary`·client-visible non-contradicted `pinned_fact`를 함께 조립한다. 3차에서는 마스킹된 client-visible 발화에서 `[NAME]`/`[ORG]` identity와 명시적 상담 약속만 보수적으로 `pinned_fact`에 upsert했고, 4차에서는 삽입 또는 값 변경 시 `pinned_fact_history`에 append-only 이력을 남긴다. 5차에서는 명시적 상담 약속 철회/부정만 기존 non-locked `agreement:counseling` fact를 `contradicted`로 격리하고 history reason `contradiction`을 남긴다. 6차에서는 세션 종료 저장 성공 뒤 마스킹된 client-visible 내담자 발화만 `app.turn_embedding`에 BGE-M3 dense/sparse로 `ON CONFLICT (turn_id) DO NOTHING` 색인한다. 7차에서는 submit/stream 공통 `_prepare_turn_context()` 경계를 만들고 DB seed recall·pinned fact가 다음 턴 EngineMessage L2/L4에 raw 이름 마스킹 상태로 주입되는 route-level 회귀를 추가했다. 8차에서는 `SessionDigestInput`/`SessionDigestResult`/`SessionSummaryWrite`로 종료 digest 입력·fallback 결과·DB write 인자 경계를 명시했다. 9차에서는 `DigestQualityAssessment`/`SessionDigestWorkerOutcome`로 LLM digest 후보 local quality harness를 추가해 빈/짧은 digest, raw forbidden substring, 내부 평가·CCD·상태 marker, 잘못된 `S{session_no}:` prefix를 fallback 유지 대상으로 판정한다. 10차에서는 `session_digest_worker.py``CompressionJob``GenerateRequest` 변환, 주입형 engine/audit 호출, accepted-only `session_summary.digest/compressed_by/token_count``case_profile.case_digest` 적용 계획, one-shot DB loader를 소유한다. 11차에서는 `scripts/run-session-digest-worker.py` dry-run/apply runner와 `SESSION_DIGEST_WORKER_ENABLED=false` 기본값의 세션 종료 background scheduler 골격을 추가하고, `compressed_by IS NULL` loader/apply CAS로 이미 압축된 세션 재실행 race를 막는다. LLM digest worker 입력 후보는 client-visible `text_masked` turn과 open thread로 제한하고, 종료 상태 수치·평가 payload·CCD·raw text는 압축 prompt에 넣지 않는다. `/sessions/{id}/end``digest_pending`은 CompressionJob 생성 여부를 알리는 비동기 압축 필요 신호로 유지한다. 새 contradicted fact는 임의 생성하지 않고, 같은 값 재확인은 history를 늘리지 않으며, `locked` fact는 건드리지 않는다. learner-owned `case_profile` 기준 RLS insert/update를 추가했고, 관계갈등·위기·임상 추론은 자동 pinning/모순 처리에서 제외했다. 관계·임상 fact 승격 기준, 실 provider 장시간 운영, 임상 골든셋 품질평가, 재압축은 후속. **검증: `py -3.11 -X utf8 -B -m pytest -p no:cacheprovider app/test_session_digest_worker.py app/test_session_memory.py -q` 30 passed, `py -3.11 -X utf8 -B -m pytest -p no:cacheprovider app/test_session_digest_worker.py app/test_session_memory.py app/test_session_turn_persistence.py app/test_orchestrator_masking.py app/test_runtime_policy.py -q` 87 passed, `py -3.11 -X utf8 -B -m py_compile app/services/session_digest_worker.py app/services/memory.py app/test_session_digest_worker.py app/test_session_memory.py`, 기존 `py -3.11 -X utf8 -B -m pytest -p no:cacheprovider app/test_session_memory.py -q` 20 passed, 과거 `py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_session_memory.py app/test_session_turn_persistence.py app/test_orchestrator_masking.py app/test_runtime_policy.py -q` 74 passed, `npm run check:api-types`, `npm run typecheck`.**
- C1 사례개념화 워크시트 4차: `SessionReviewResponse.caseWorksheet`와 리뷰 화면 카드가 탐색 11항목·호소 5영역·인지삼제/감정·보호/방해·생물심리사회 목표 초안을 축어록 근거 기반으로 제공한다. 학습자가 편집한 저장본은 `PUT /sessions/{id}/review/worksheet``app.case_worksheet`에 저장되고, 이후 `GET /review``saved_by_learner` 저장본을 자동 초안보다 우선 반환한다. 2026-06-28에는 `data/rubrics/case-worksheet-rubric.json`, schema, loader/validator service, `scripts/check-case-worksheet-rubric.py`를 추가해 임상팀 확정 루브릭을 외부 JSON으로 받을 scaffold를 만들었다. 현재는 `status=scaffold_only`, `scoring_enabled=false`이며 5개 section/28개 item key와 sha256 provenance만 검증한다. 64차 refactor-governance 패스에서 워크시트 템플릿 key source를 `CASE_WORKSHEET_SECTION_SPECS`/`case_worksheet_template_item_keys()`로 명시해 CLI/test가 더미 턴 없이 같은 생성 spec을 검증하게 했다. 73차에서는 기존 `app.session_review_status``worksheet_status`/`worksheet_note`/`worksheet_reviewed_at`을 추가하고, `PUT /teacher/sessions/{id}/review-status`와 교수자 `SessionReview` 카드에서 승인·수정요청·반려 수동 판정을 저장/표시한다. 임상팀 확정 루브릭 콘텐츠, AI 추출/채점 적용·calibration, 승인 후 잠금·재제출 정책, 재귀학습 데이터셋 approved 연계는 후속 gate다. **검증: `py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_session_turn_persistence.py app/test_teacher_dashboard.py app/test_rbac_idor.py app/test_case_worksheet_rubric.py -q` 40 passed, `py -3.11 -X utf8 scripts/check-case-worksheet-rubric.py --json` PASS, `py -3.11 -X utf8 -m py_compile` focused files, `npm run generate:api-types`, `npm run check:api-types`, `npm run typecheck`, `npm run build`, `npx playwright test e2e/session-review.spec.ts --project=chromium-desktop --workers=1` 3 passed, 로컬 API smoke 세션 생성→저장→`GET /review` `saved_by_learner:local smoke saved worksheet`.**
- C3 이론모드 2차: `theory_mode``TurnContext`/sessions/voice/evaluator뿐 아니라 `build_turn_messages`의 엔진 메시지까지 전달된다. 프론트는 `persona.theory_target` 기준 기본값을 유지하되, 세션 시작 전 `humanistic`/`cbt`/`integrative` segmented control로 학습자가 명시 선택하고 `POST /sessions``theory_mode`로 보낸다. 2026-07-01에는 교수자 수동 session/turn 재평가가 persona 기본 `theory_target`보다 session `theory_mode`를 우선하고, turn 재평가 `TurnContext`에도 같은 값을 채우도록 보강했다. CBT 체인·이론부합 루브릭은 후속. **검증: `python -B -m pytest -p no:cacheprovider app/test_orchestrator_masking.py app/test_session_turn_persistence.py -q` 31 passed, `C:\Users\encep\AppData\Local\Programs\Python\Python311\python.exe -X utf8 -m pytest -p no:cacheprovider app/test_eval_routes.py -q` 5 passed, `C:\Users\encep\AppData\Local\Programs\Python\Python311\python.exe -X utf8 -m pytest -p no:cacheprovider app/test_eval_routes.py app/test_notifications.py app/test_session_turn_persistence.py -q` 39 passed, `npm run typecheck`, `npm run build`, `npx playwright test e2e/session-layout.spec.ts --project=chromium-desktop --workers=1` 4 passed, `npx playwright test e2e/session-layout.spec.ts --project=chromium-mobile --workers=1` 4 passed, `npx playwright test e2e/session-mvp.spec.ts --project=chromium-single-run --workers=1` 1 passed, `npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run --workers=1` 9 passed.**
- M3 인증 claim 1차: Google/SAML/dev-login이 `AUTH_EMAIL_COHORT_MAP`·`AUTH_DOMAIN_COHORT_MAP` 및 SAML cohort claim을 `cohort_ids`로 전달하고, DB `app_user.external_id`는 provider subject(`google:`/`saml:`/`dev:`) 기반으로 저장한다. 운영 SAML 서명검증, 기관 claim schema/test tenant, deprovisioning audit은 후속. **검증: `pytest app/test_auth_providers.py -q` 30 passed, `pytest app/ -q` 178 passed.**
- X2 예산 경고/저비용 평가 라우팅/evaluator cache 관측·일별 비용 추이·모델별 비용 검증 리포트 2차: `ADMIN_USAGE_BUDGET_USD` 설정값을 기준으로 `GET /admin/usage``budget.status=disabled|ok|warn|exceeded`, 사용률, 잔여 예산을 반환하고 관리자 콘솔이 예산 상태 배너를 표시한다. 80% 이상 warn, 100% 이상 exceeded. `EVALUATOR_FAST_MODEL`/`EVALUATOR_DEEP_MODEL` 설정 시 fast/deep 평가 호출만 해당 모델 override로 gateway에 전달하고, 비워두면 기존 gateway default 라우팅을 유지한다. fast/deep evaluator structured 결과는 canonical request SHA-256 기반 인메모리 semantic cache로 재사용하며, 원문 prompt·completion은 저장하지 않고 성공 파싱 결과만 TTL/entry 제한 안에서 캐시한다. `/admin/usage`와 관리자 `AI 비용` 카드는 cache enabled/entries/hits/misses/stores/evictions/requests/hit_rate와 일별 `daily_cost` 추이를 표시하되 cache key·prompt·completion은 노출하지 않는다. `app.services.usage_report``scripts/report-ai-usage.py`는 동일 usage JSON에서 provider/model별 cost share, token share, cost/turn, cost/1k tokens, metered coverage, budget/cache warning을 산출한다. 자동 차단·한도 enforcement 정책은 후속. **검증: `python -B -m py_compile app/services/usage_report.py app/test_usage_report.py ..\..\scripts\report-ai-usage.py`, `python -B -m pytest -p no:cacheprovider app/test_usage_report.py app/test_evaluator_model_routing.py app/test_runtime_policy.py app/test_admin_ops.py -q` 42 passed, `scripts/report-ai-usage.py` sample CLI schema `vignette.ai_usage_model_cost_report.v1` 생성, prior `npm run check:api-types`, `npm run typecheck`, `npm run build`, admin E2E evidence remains valid because API response shape was not changed.**
- H4 LLM call audit/마스킹 2차: 상담 생성(generate/stream)·fast-loop 평가·deep-loop 평가의 외부 LLM 호출 직후 `audit.llm_call_log`에 provider/model/token/cost/inference_geo/latency metadata만 적재한다. prompt/completion 본문은 저장하지 않고 감사 실패는 상담 루프를 막지 않는다. 2026-06-28에는 한국어 이름/기관 로컬 휴리스틱 마스킹 1차를 추가했고, fast evaluator prompt의 내담자 응답과 client turn `text_masked`도 마스킹본을 쓰게 보강했다. 이어서 `app.services.pii_masking_eval`, `data/privacy/pii-masking-ko-fixtures.json`, `scripts/evaluate-pii-masking.py`로 합성 fixture 15케이스 평가 harness를 추가했고, `data/privacy/pii-masking-eval-input.schema.json``data/privacy/pii-masking-eval-report.schema.json`으로 source/category/severity metadata와 summary-only `technical_dry_run` report 계약을 고정했다. 66차에서는 `제 이름은 김서연입니다`, `보호자 이름은 박민수입니다`, `저는 최하늘입니다` 자연 발화형 이름 라벨·자기소개 케이스와 `이름은 중요하지 않다` negative control을 추가해 라벨 단어만 마스킹하고 실명을 남기던 구멍을 막았다. 70차에서는 `guardrail.mask_pii()` 내부에 선택형 한국어 PII recognizer adapter 경계를 추가했다. adapter는 import-time hard dependency가 아니며 명시 등록 전에는 비활성이고, 실패해도 기존 regex fallback이 마지막 안전망으로 유지된다. fake adapter 테스트는 regex가 못 잡는 별명/기관 span을 `[NAME]`/`[ORG]`로 마스킹하고 같은 문장의 전화번호는 후단 regex가 `[PHONE]`으로 처리하는지, adapter 실패 시에도 fallback이 유지되는지 검증한다. span 중복 처리는 문자별 set 할당 대신 정렬된 `last_end` 비교로 정리했다. 기본 CLI JSON은 `masked_text`/`forbidden_remaining` 원문 증거를 제외하며, `소속`/`안내`/`이름` NAME 오탐도 stopword로 보정했다. 로컬 dev-login 실제 `/turn` smoke에서 `audit.llm_call_log`가 8→11로 3행 증가했다(session `2460d56c-a9cb-4a40-a175-9575d510a5e9`). 공개 Google OAuth 실제 `/turn` proof는 별도 B2 항목에 남긴다. **검증: `py -3.11 -X utf8 -m py_compile apps/api/app/services/guardrail.py apps/api/app/services/pii_masking_eval.py apps/api/app/test_pii_masking_eval.py apps/api/app/test_orchestrator_masking.py scripts/evaluate-pii-masking.py`, `py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_pii_masking_eval.py app/test_orchestrator_masking.py app/test_evaluation_persistence.py app/test_session_turn_persistence.py -q` 47 passed, `py -3.11 -X utf8 scripts/evaluate-pii-masking.py --json` 15/15 pass, 과거 전체 기준 `pytest app/ -q` 119 passed.**
- H4 온보딩·동의 게이트 3차: `app_user`에 이름·소속·학과·학년/직위·전화번호·주소/수령지·닉네임·자기소개·아바타 URL·약관/개인정보 동의 시각·버전 필드를 추가했다. 로그인 직후 `/onboarding` 완료 전에는 전역 `OnboardingGate``/onboarding` 외 모든 앱 URL(`/learn`, `/settings`, `/admin`, `/dev/avatar-preview`, `/login` 포함)을 온보딩으로 이동시키고, 온보딩 중에는 공용 셸 메뉴 없이 가입 정보 입력 폼만 보여준다. learner `POST /sessions`와 voice dev persona 시작은 `onboarding_required`를 먼저 확인하고, 온보딩 저장 시 learner `consent_at`도 함께 세팅한다. 아바타는 `/users/me/avatar`에서 PNG/JPG/WebP MIME·시그니처·3MB 제한을 통과한 파일만 `USER_UPLOAD_DIR/profile-avatars`에 저장하고 DB에는 URL만 보관한다. 약관·개인정보 처리방침은 개인정보 보호법·처리방침 작성지침·안전성 확보조치 기준·약관규제법 취지를 반영한 개발 초안으로 `/users/legal-docs`에서 제공한다. 관리자/교수자 권한은 온보딩 화면에서 신청받지 않고 기존 `AUTH_ADMIN_EMAILS`/`AUTH_TEACHER_EMAILS` allowlist 및 관리자 사용자 관리 경로로만 부여한다. 한국어 이름/기관은 로컬 휴리스틱 마스킹, optional ko recognizer adapter 배선, 합성 fixture 평가 harness까지 완료했고, 실제 ko recognizer 모델/provider 선정, 운영 말뭉치 기반 오탐/미탐 평가, guardian/legal 최종 서명 동의서 evidence, 공개 Google OAuth 실제 `/turn` proof는 후속. **검증: `pytest app/ -q` 178 passed, `pytest engine_gateway/ -q` 11 passed, `npm run check:api-types`, `npm run typecheck`, `npm run build`, `npx playwright test e2e/auth.spec.ts --project=chromium-desktop --workers=1` 7 passed, `PLAYWRIGHT_PORT=5174 npm run e2e` 113 passed.**
- RAG warm 동시성 안정화: layout/session E2E가 여러 세션을 빠르게 만들 때 BGE-M3 embedder가 동시에 여러 번 지연 로드되며 `tqdm` lock 예외와 API health/dev-login timeout이 반복됐다. `rag.py`에서 embedder load/encode를 process-wide `RLock`으로 직렬화하고, `sessions.py``_warm_rag_caches`를 semaphore 1개로 제한해 warm task가 상담 요청 경로를 막지 않도록 했다. **검증: `pytest app/ -q` 125 passed, `npx playwright test e2e/layout-visual-gate.spec.ts e2e/session-layout.spec.ts --project=chromium-single-run --project=chromium-desktop --workers=1` 11 passed.**
- 공개/로컬/Tailnet 로그인 복구: public API 530 원인은 prod에서 개발 전용 `VIGNETTE_VOICE_POC_SAMPLE_TTS=true`가 fail-close된 것과 DB `app.admin_engine_config` 기본 행 부재였다. 2026-06-27 모바일 502는 cloudflared 로그의 `127.0.0.1:8001` origin refused와 일치했다. `scripts/start-public-runtime.ps1`는 public prod 기동 시 샘플 TTS를 강제로 끄고, 운영 DB에는 `claude_cli`/`127.0.0.1:9099` engine config 행을 복구했다. 추가로 dev/Tailnet에서는 public OAuth callback이 로컬/Tailnet 세션으로 붙지 않으므로 Google 버튼과 직접 시작 URL을 `local_oauth_unavailable`로 차단하고 dev-login만 사용한다. public OAuth state는 HttpOnly 쿠키에 묶인 HMAC 서명 토큰으로 복구해 API 재시작 뒤 콜백이 `invalid_state`로 떨어지지 않는다. callback 실패는 비밀값 없는 reason/status로 남기고, 로그인 화면도 token/state/provider/identity 실패 메시지와 reason code를 분리한다. 2026-06-27 18:57 KST에 Tailnet stale Vite `allowedHosts` 재발 상태를 재확인했고, `alpaca-home.taile93291.ts.net`를 Vite 기본 허용 host에도 포함해 수동 Vite 기동 시 403 재발 가능성을 낮췄다. **검증: `https://vignette.chanpaca.net/login` 200, `https://api-vignette.chanpaca.net/health` prod/db/engine true, public Google auth redirect 302 + HttpOnly state cookie, provider callback error → `/login?oauth=access_denied`, `https://alpaca-home.taile93291.ts.net/login` 200 + `/api/health` dev/db/engine true + `/api/auth/config` 200, env 없는 임시 Vite host-header smoke 200, local/Tailnet Google direct는 `local_oauth_unavailable`, local auth E2E 7 passed, Tailnet auth/dev-login E2E 2 passed.**
- Docker dev DB 안전장치: `scripts/dev-up.ps1``vignette-dev-db` healthcheck/pg_isready/role safety를 점검하고, 새 컨테이너 생성 시 `vignette_owner`와 앱 role을 분리해 healthcheck를 붙인다. `infra\.env` compose 필수값 누락도 사전 경고한다. `scripts/dev-down.ps1`은 기본 DB 보존, `-Db` 지정 시 컨테이너 중지로 정리했다. **검증: 현재 Docker DB accepting connections, `vignette_app:false:false`, 기존 컨테이너 healthcheck 없음. 패키징 smoke에서 fresh compose DB는 owner/app role 분리 상태로 API healthy까지 통과했다. 기존 dev DB는 `POSTGRES_USER=vignette` 기반이라 운영 role 전환은 별도 owner migration 확인 후 진행한다.**
- Docker 패키징 정리 1차: API Dockerfile은 저장소 루트 컨텍스트에서 `apps/api``data`를 함께 복사하고, 런타임 코드의 `Path(__file__).parents[...]` 루트 계산은 `app.paths.repo_root()`로 모았다. 웹 Dockerfile은 npm lockfile 기반 `npm ci`/`npm run build`로 고정하고, web/root `.dockerignore`로 host `node_modules`와 런타임 산출물이 이미지에 섞이지 않게 했다. 구현 없는 `rag.server` 사이드카는 compose에서 제거하고, RAG 무거운 의존성은 `INSTALL_RAG=true` build arg로 API 이미지에 opt-in 설치한다. 사용자 업로드는 `apiuploads` 볼륨(`/app/uploads`)으로 분리했다. API 이미지에 로컬 `apps/api/.env`가 복사되어 prod fail-closed 설정을 오염시키던 보안 결함도 `.dockerignore`로 차단했다. API startup은 init-complete DB에서 owner 권한 DDL을 건너뛰고, non-dev에서 schema가 미완성이면 owner-run init/migration과 `scripts/check-deploy-preflight.py`를 요구한다. `admin_engine_config` 행이 없으면 env 기본 엔진 설정으로 기동한다. backend base/RAG requirements는 exact version으로 고정했고, `data/kb` live-coaching source pack은 commit 또는 동일 경로 provision이 필요한 런타임 입력으로 문서화했다. **검증: `docker compose -f infra/docker-compose.yml config --quiet` 더미 필수 env 통과, API/Web image build 통과, API image `.env*` 미포함, prod import smoke 통과, fresh `docker compose up -d --build` smoke 통과(`/api/health` 200, `db:true`, `engine:true`, `api healthy`), `python scripts/check-deploy-preflight.py --skip-db --env-file infra\.env.example --allow-placeholder-secrets` 통과. 실제 이식 전에는 배포지 `infra/.env``APP_DB_PASSWORD`, OAuth client id/secret, `OPENAI_API_KEY`, `SESSION_SECRET` 등 소유자 비밀값을 채워야 한다.**

View file

Before

Width:  |  Height:  |  Size: 882 KiB

After

Width:  |  Height:  |  Size: 882 KiB

Before After
Before After

File diff suppressed because one or more lines are too long

View file

@ -250,10 +250,15 @@ RBAC×AIView로 차단된다. 이 모듈은 평가 신호만 산출한다.
content_hash 기반으로 증분 색인한다. `app.services.source_pack_sync`가 active `kb.document`
hash/version을 먼저 읽고, hash 변경 시 새 document version을 최신+1로 계산한다. 기본 visibility는
evaluator 전용이고, C/D·diagnostic 자료는 sensitivity 2로 고정한다.
- 출력은 `LiveCoachSuggestion` 구조화 JSON(`tone/focus/title/message/next_utterance/rationale/sources`).
UI는 코칭 아바타 말풍선, 근거 모달, 발화별 코칭 이력 오버레이로 표시한다.
- 전달된 코칭은 `app.live_coach_events`에 저장된다. 원문 축어록을 중복 저장하지 않고 PII 마스킹된 짧은
learner/client excerpt와 코칭 payload만 저장한다. DB 미가용 dev에서는 런타임 캐시로 폴백한다.
- 출력은 `LiveCoachSuggestion` 구조화 JSON(`tone/focus/title/message/next_utterance/rationale/sources/quota/credit_events`)
형태가 아니라 `quota``credit_events`를 포함한 짧은 카드 계약이다. UI는 코칭 아바타 말풍선,
근거 모달, 발화별 코칭 이력 오버레이로 표시한다.
- 회기별 코칭 기회는 기본/최대 3개다. `POST /sessions/{id}/live-coach` 사용 시 `use/-1` 이벤트를 남기고,
턴 평가가 `appropriateness=pos`, `rapport_signal>=0.35`, 그리고 단계 전환 또는 `effective_openness`
상승을 동시에 만족하면 `recharge/+1` 이벤트로 1개를 재충전한다(최대 3개). 사용권 없음은 409로 막는다.
- 전달된 코칭과 충전/사용 기록은 `app.live_coach_events`에 저장된다. 원문 축어록을 중복 저장하지 않고
PII 마스킹된 짧은 learner/client excerpt, 코칭 payload, `event_type/credit_delta/credit_balance/reason`
저장한다. DB 미가용 dev에서는 런타임 캐시로 폴백한다.
- DSM/공식 지침/논문은 사용 허가된 source pack으로 투입할 수 있다. 다만 코칭 프롬프트와 UI에는
장문 원문이나 공식 문항을 재현하지 않고, chunk summary + version/citation + 출처 식별자로 노출한다.
@ -336,9 +341,11 @@ session lifecycle을 유지하며, future Node read API는 이 read-model contra
- `POST /sessions/{id}/stream` — SSE 경로(`run_turn_stream`). token/done/ping/error를 흘리고,
done 시점에 학습자 발화 + 누적 내담자 응답을 영속화. `sse_heartbeat_seconds`마다 ping(Cloudflare 타임아웃 회피).
- `POST /sessions/{id}/live-coach` — 방금 완료된 상담자 발화를 워크북 요약/RAG/evaluator fast-loop 신호와 대조해
라이브 코칭 카드 1개를 반환하고 `app.live_coach_events`에 저장한다. 저장 payload는 마스킹 excerpt와 코칭 구조화 JSON이다.
라이브 코칭 카드 1개를 반환하고 `app.live_coach_events``use/-1`로 저장한다. 저장 payload는 마스킹
excerpt와 코칭 구조화 JSON이며, 잔여 기회가 없으면 409로 차단한다.
- `GET /sessions/{id}/live-coach` — 현재 회기에서 학습자에게 실제 전달된 코칭 이력을 시간순으로 반환한다.
세션 화면은 이 응답을 `turn_seq`별로 묶어 학습자 발화 우측 코칭 마커와 채팅 위 스크롤 오버레이에 표시한다.
세션 화면은 이 응답을 `turn_seq`별로 묶어 학습자 발화 우측 코칭 마커와 채팅 위 스크롤 오버레이에 표시하고,
`quota`/`credit_events`로 사용권 점 표시와 사용·충전 애니메이션을 갱신한다.
- `POST /kb/live-coach/source-packs/sync` — 관리자 전용. 로컬 라이브 코칭 source pack을 `kb.source` upsert 후
`kb.document/kb.chunk`로 색인한다. active hash가 같으면 skip하고, 다르면 최신 document version+1로 색인한다.
임베딩 모델이 없으면 BM25-only degraded 색인으로 진행한다.
@ -375,6 +382,8 @@ session lifecycle을 유지하며, future Node read API는 이 read-model contra
- `GET /teacher/dashboard` — 담당 학습자 성장, 안전 알림, 종료 회기 검토 큐를 반환한다. 회기 요약에는
`review_status`, `review_note`, `reviewed_at`를 포함해 교수자가 이미 검토한 회기를 구분한다.
학습자 표시는 `app.app_user.display_name`/`nickname`/`email`을 우선 사용하고, 사용자 row가 없을 때만
축약 learner id로 폴백한다.
- `GET /teacher/learners/{learner_id}/analysis` — 담당 범위 안의 특정 학습자 전체 회기 타임라인을
오래된 순서로 반환한다. `summary`/`points`는 제한 없는 사용자별 추이를 담고, `stage_breakdown`
라포·탐색·개입·정리 단계별 회기 수와 턴 수를 담는다. 담당 범위 밖 learner id는 404로 닫는다.
@ -384,7 +393,9 @@ session lifecycle을 유지하며, future Node read API는 이 read-model contra
`/teach/analysis` 학생 분석은 별도 메뉴/라우트로 분리되어 전체 담당 학습자를 검색 가능한
1행 테이블로 먼저 보여주고, 우측 펼침으로 행 아래 미니 추이·기법·최근 기록을 확장한다.
학습자 이름 또는 상세 보기 버튼은 `?learner=` 상세 드릴다운으로 이동하며, 상세 화면은
추이·전체 회기·단계 분석 탭으로 선택 학습자의 전체 이력을 보여준다. `/teach/session/:sessionId/review` 화면은 같은
페르소나별 회기를 기본 탭으로 먼저 보여준다. 각 페르소나 행은 회기 수·평균 적절성·라포·검토
카운트를 요약하고 펼침으로 해당 페르소나의 회기 목록을 연다. 추이·전체 회기·단계 분석 탭은 같은
상세 화면에서 선택 학습자의 전체 이력을 보조한다. `/teach/session/:sessionId/review` 화면은 같은
`GET /sessions/{id}/review` 자료를 교수자 읽기 전용으로 표시하고, 검토 메모 저장은 위 teacher endpoint로 분리한다.
### 2.9.2 운영 메일 알림 — `app/services/notifications.py`

File diff suppressed because one or more lines are too long

View file

@ -15,12 +15,12 @@ Vignette 저장소의 모든 검증 수단(백엔드 단위 테스트, 웹 타
| 검증 | 작업 디렉터리 | 명령 | DB | API(8000) | 웹(5173) | 엔진GW(9099) | 브라우저 | 현재 통과 |
|---|---|---|---|---|---|---|---|---|
| 백엔드 단위 테스트 | `apps/api` | `python -m pytest app/ -q` | 불필요 | 불필요 | 불필요 | 불필요 | 불필요 | 현재 309 pass |
| 백엔드 단위 테스트 | `apps/api` | `python -m pytest app/ -q` | 불필요 | 불필요 | 불필요 | 불필요 | 불필요 | 현재 collect-only 349 tests, 최신 focused pass |
| 엔진 게이트웨이 테스트 | `apps/api` | `python -m pytest engine_gateway/ -q` | 불필요 | 불필요 | 불필요 | 불필요 | 불필요 | 현재 27 pass |
| API 타입 생성 체크 | `apps/web` | `npm run check:api-types` | 불필요 | 불필요 | 불필요 | 불필요 | 불필요 | pass |
| 웹 타입체크 | `apps/web` | `npm run typecheck` | 불필요 | 불필요 | 불필요 | 불필요 | 불필요 | pass |
| 웹 빌드 | `apps/web` | `npm run build` | 불필요 | 불필요 | 불필요 | 불필요 | 불필요 | pass |
| Playwright E2E(전체) | `apps/web` | `npm run e2e` | **필요(+시드)** | **필요** | 자동기동 | 일부만 | **필요** | 166 tests / 18 files |
| Playwright E2E(전체) | `apps/web` | `npm run e2e` | **필요(+시드)** | **필요** | 자동기동 | 일부만 | **필요** | 194 tests / 19 files |
핵심 원칙: **단위 테스트(pytest)와 타입체크/빌드는 외부 서비스 없이 단독 실행된다.**
**E2E만 풀스택(DB+API+웹+브라우저)을 요구한다.** 아래 각 절에서 근거와 절차를 설명한다.
@ -42,14 +42,14 @@ Vignette 저장소의 모든 검증 수단(백엔드 단위 테스트, 웹 타
```sh
# apps/api
python -m pytest app/ -q # 앱 단위 테스트 현재 309 pass
python -m pytest app/ -q # 앱 단위 테스트: 현재 collect-only 349 tests, 최신 focused pass
python -m pytest engine_gateway/ -q # 게이트웨이 단위 테스트 현재 27 pass
```
수집만 빠르게 확인하려면:
```sh
python -m pytest app/ --collect-only -q # 현재: "305 tests collected"
python -m pytest app/ --collect-only -q # 현재: "349 tests collected"
python -m pytest engine_gateway/ --collect-only -q # 현재: "27 tests collected"
```
@ -154,9 +154,11 @@ GitHub Actions `.github/workflows/api-contract.yml`은 API/Web 계약 관련 파
### 3.1 의존성 — 왜 풀스택인가
E2E는 Playwright route fixture로 앱 데이터를 대체하지 않고, **Vite `/api` 프록시를 통해 실제
로컬 FastAPI를 호출한다**(`apps/web/e2e/README.md`). 특히 공용 헬퍼
`apps/web/e2e/support.ts``fetchAvailablePersonas()`
Playwright suite에는 **Vite `/api` 프록시를 통해 실제 로컬 FastAPI/DB를 호출하는 full-stack
E2E**와, 특정 UI/error 상태를 고정하는 **route fixture UI 회귀 테스트**가 함께 있다
(`apps/web/e2e/README.md`). DB-backed/real-API 증거는 해당 spec이 검증 대상 endpoint를
`page.route().fulfill()`로 대체하지 않고 실제 API 응답 또는 persisted read-model을 확인한
경우로 한정한다. 특히 공용 헬퍼 `apps/web/e2e/support.ts``fetchAvailablePersonas()`
`source === "database" && !degraded` 페르소나만 사용 가능으로 간주한다.
```ts
@ -258,17 +260,43 @@ VITE_API_BASE=http://127.0.0.1:8000 npm run e2e # 프록시 대신 API
### 3.6 실측 테스트 개수 (현재)
`npx playwright test --list` 기준 **총 166 tests / 18 files**.
`npx playwright test --list` 기준 **총 194 tests / 19 files**.
- **병렬 시나리오**: 130 tests (desktop 65 + mobile 65)
- **`@single-run` 직렬 시나리오**: 36 tests (DB 영속화·세션 MVP·음성 성공경로·회기말 평가 저장·교수자 사용자별 분석 등)
- **병렬 시나리오**: 150 tests (desktop 75 + mobile 75)
- **`@single-run` 직렬 시나리오**: 44 tests (DB 영속화·세션 MVP·음성 성공경로·회기말 평가 저장·교수자 명시 재평가 저장·교수자 턴 재평가 저장·교수자 UI 평가 재시도·교수자 사용자별 분석·source pack sync·이론모드 저장·동의 철회 후 voice 차단·브라우저 stream PII 마스킹·음성 transcript 저장 실패 UI 표면화 등)
- `e2e/voice-success.spec.ts`는 직접 `/voice/ws` 캐스케이드와 Session 마이크 UI를 함께 검증하며,
브라우저 `<audio>.play()`가 차단된 조건에서도 Web Audio buffer source 재생이 시작되는지와
Session 마이크 UI가 `audio_end`에 browser voice activity/silence 메타를 싣는지 확인한다.
- 2026-07-01 focused 검증: `PLAYWRIGHT_PORT=5205 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1` **7 passed**. 실제 브라우저 `openSessionStream()``/sessions/{id}/stream` → DB-backed `/review` 축어록 저장 경로, AI 튜터 코칭 이력 저장/재로딩, WebSocket `stt_result` 음성 비언어 메타데이터, Session 마이크 UI가 생성한 voice activity/silence 메타, Phase 3 pre/post 점수의 DB-backed 저장/재조회, 그리고 세션 종료 background deep 평가가 durable DB row로 저장되어 교수자 리뷰가 `평가 완료`로 전환되는지 검증한다.
- 2026-07-01 focused 검증: `PLAYWRIGHT_PORT=5205 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1` **7 passed**. 실제 브라우저 `openSessionStream()``/sessions/{id}/stream` → DB-backed `/review` 축어록 저장 경로, AI 튜터 코칭 이력 저장/재로딩, WebSocket `stt_result` 음성 비언어 메타데이터, Session 마이크 UI가 생성한 voice activity/silence 메타, Phase 3 pre/post 점수의 DB-backed 저장/재조회, 그리고 세션 종료 background deep 평가가 durable DB row로 저장되어 교수자 리뷰가 `평가 완료`로 전환되는지 검증한다. AI 튜터 코칭 이력 검증은 `POST /live-coach` 응답과 DB-backed history payload의 `status=ready`, `latency_ms>0`도 확인해 규칙 기반 `degraded` fallback 200 응답이 정상 AI 코칭으로 통과하지 못하게 한다.
- 2026-07-01 추가 DB-backed 검증: `PLAYWRIGHT_PORT=5238 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1 --grep "finishes session end evaluation"` **1 passed**. 같은 자동 종료 평가 row가 `/teacher/dashboard``recent_sessions`에서도 `evaluation_status=ready`, `review_ready=true`, `supervisor_state=평가 완료`로 반영되는지 실제 DB/API/엔진으로 검증한다.
- 2026-07-01 추가 DB-backed 검증: `PLAYWRIGHT_PORT=5237 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1 --grep "explicit teacher session reevaluation"` **1 passed**. 실제 DB/API/엔진에서 학습자 세션과 턴을 만든 뒤 교수자 `POST /eval/sessions/{id}/reevaluate`가 durable `app.session_evaluation` row를 저장하고, `/eval/.../evaluation``/review``평가 완료`로 반영되는지 검증한다.
- 2026-07-01 추가 DB-backed 검증: `PLAYWRIGHT_PORT=5246 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1 --grep "teacher turn reevaluation"` **1 passed**. 실제 DB/API/엔진에서 학습자 세션과 턴을 만든 뒤 교수자 `POST /eval/sessions/{id}/turn`이 기존 turn evaluation normalized row를 durable 교체하고, `/review`의 learner turn `techniques`가 재평가 응답과 같은 라벨로 hydrate되는지 검증한다.
- 2026-07-01 추가 DB-backed UI 검증: `PLAYWRIGHT_PORT=5245 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1 --grep "manual AI evaluation retry"` **1 passed**. admin engine config를 잠깐 실패 endpoint로 바꿔 실제 `app.session_evaluation` 실패 row를 만든 뒤 원복하고, 교수자 리뷰 UI의 `AI 평가 재시도` 버튼 클릭이 실제 `POST /eval/sessions/{id}/reevaluate` → durable ready row → `/review` `평가 완료``/teacher/dashboard` `evaluation_status=ready`까지 이어지는지 검증한다.
- 2026-07-02 focused 검증: `PLAYWRIGHT_PORT=5255 npx playwright test e2e/kb-source-packs.spec.ts --project=chromium-single-run --workers=1` **1 passed**. 관리자 전용 `POST /kb/live-coach/source-packs/sync`가 인증 없이 401, learner 403, admin 202로 동작하고, 0615 워크북·DSM·공식 상담 지침·자살위험 지침 source pack을 DB-backed evaluator RAG에 sync한 뒤 source-scoped `/kb/eval-grounding`이 503 skip 없이 200을 반환하고 같은 `source_id`만 반환하는지 검증한다.
- 2026-07-02 focused 검증: `PLAYWRIGHT_PORT=5259 npx playwright test e2e/session-mvp.spec.ts --project=chromium-desktop --workers=1 --grep "AI tutor|stale empty AI tutor quota|quota exhaustion|degraded AI tutor|voice conversation stop"` **7 passed**. route-fixture UI에서 AI 튜터의 stale empty quota가 서버 `GET /live-coach` 재조회 뒤 실제 잔여 기회로 복구되는지, 최신 quota exhaustion 오류가 이전 코칭 카드에 가려지지 않는지, 음성 `reply.conversation_stopped`가 빈 client reply 대신 109 안전 게이트와 socket close를 유지하고 live-coach를 호출하지 않는지 고정한다. 이 fixture들은 UI 회귀 증거이며 DB-backed 엔진 성공 증거가 아니다.
- 2026-07-01 focused 검증: `PLAYWRIGHT_PORT=5251 npx playwright test e2e/session-mvp.spec.ts --project=chromium-desktop --workers=1 --grep "AI tutor|runtime AI tutor"` **4 passed**. mock MVP 코칭 카드에서 `근거 보기` 모달이 source title, `source_pack` kind, `2026-06-15` version, citation을 표시하는지 검증하고, `status=degraded` route fixture는 카드·근거 모달·이력 모달·턴 `C` 마커가 `대체 코칭`/`AI 응답 대체`를 노출하는지 고정한다. 추가 route fixture는 `GET /live-coach` 실패가 빈 이력으로 보이지 않고 alert로 표시되는지, `persistence_source/source=runtime`이 임시 저장 경고로 표시되는지도 고정한다. 이 fixture들은 UI 회귀 증거이며 DB-backed 엔진 성공 증거가 아니다.
- 2026-07-01 focused 검증: `PLAYWRIGHT_PORT=5252 npx playwright test e2e/session-review.spec.ts --project=chromium-desktop --workers=1 --grep "pre/post"` **1 passed**. 기존 저장 pre/post 점수를 비운 입력이 `저장된 값 기준`으로 오인되지 않고 invalid 상태, alert, 저장 버튼 disabled로 표면화되는지 검증한다.
- 2026-07-01 focused 검증: `PLAYWRIGHT_PORT=5248 npx playwright test e2e/session-mvp.spec.ts --project=chromium-single-run --workers=1 --grep "pending voice transcript"` **1 passed**. mock WebSocket이 `transcript final``turn_persistence_unavailable` error를 보내면 Session UI가 임시 학습자 발화를 정상 턴으로 확정하지 않고 `저장 실패` 배지와 alert로 표면화하며, 대기 중 내담자 말풍선을 제거하고 텍스트 입력을 복구하는지 검증한다.
- 2026-07-01 추가 DB-backed 검증: `PLAYWRIGHT_PORT=5241 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1 --grep "AI tutor coaching history"` **1 passed**. 실제 DB/API/엔진 경로에서 AI 튜터 응답이 `status=ready`, `latency_ms>0`로 저장되고, 0615 source pack metadata가 `근거 보기` 모달, DB-backed `/live-coach` 이력, reload 후 `C` 마커 history dialog에 유지되는지 확인한다.
- 2026-07-01 fixture UI focused 검증: `PLAYWRIGHT_PORT=5229 npx playwright test e2e/session-review.spec.ts --project=chromium-desktop --workers=1 --grep "retry a failed AI session evaluation|retry fails"` **2 passed**. 교수자 리뷰의 `평가 실패` 상태에서 `AI 평가 재시도` 버튼이 보이고, 재시도 성공 시 완료로 바뀌며 재시도 실패 시 기존 실패 리뷰와 새 실패 사유가 유지되는지 검증한다.
- 2026-07-01 추가 fixture UI/mixed focused 검증: `PLAYWRIGHT_PORT=5236 npx playwright test e2e/session-review.spec.ts e2e/teacher.spec.ts --project=chromium-desktop --workers=1 --grep "manual AI retry|retry a failed AI session evaluation|retry fails|failed AI session evaluation"` **4 passed**. 평가가 아직 `평가 대기`로 자동 polling 중일 때는 수동 `AI 평가 재시도` 버튼을 숨기고, `평가 실패`일 때만 재시도 버튼을 노출하며, 교수자 pending queue에서도 `하린`/`P6` 종료 회기의 `평가 실패` AI 상태가 수동 `검토 대기` 상태에 묻히지 않는지 검증한다.
- 2026-07-01 추가 fixture UI focused 검증: `PLAYWRIGHT_PORT=5253 npx playwright test e2e/session-review.spec.ts --project=chromium-desktop --workers=1 --grep "long-running session evaluation"` **1 passed**. 긴 회기 평가가 기존 짧은 polling window를 넘겨 늦게 ready가 되어도 리뷰 화면이 `평가 대기`에 고착되지 않고 `평가 완료` 리뷰를 반영하는지 검증한다.
- 2026-07-02 추가 focused 검증: `C:\Users\encep\AppData\Local\Programs\Python\Python311\python.exe -X utf8 -m pytest -p no:cacheprovider apps/api/app/test_teacher_dashboard.py apps/api/app/test_session_turn_persistence.py -k "teacher_cannot_close_review_before_ai_session_evaluation_ready or teacher_can_mark_session_review_closed_with_note or end_session_does_not_reschedule_evaluation_for_already_ended_session or session_evaluation or stale_missing" -q` **8 passed** + `PLAYWRIGHT_PORT=5254 npx playwright test e2e/session-review.spec.ts --project=chromium-desktop --workers=1 --grep "manual AI retry|long-running session evaluation|retry a failed AI session evaluation|retry fails"` **4 passed**. 오래된 missing session evaluation이 교수자 대시보드에서도 `평가 실패`로 보이고, 이미 종료된 세션의 중복 `/end`가 평가를 재예약하지 않으며, AI 평가가 ready가 아니면 교수자 리뷰를 `검토 완료`로 닫아 큐에서 숨길 수 없도록 검증한다.
- 2026-07-01 focused 검증: `PLAYWRIGHT_PORT=5229 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1 --grep "selected CBT theory mode"` **1 passed**. 실제 Session UI에서 `CBT` 이론모드를 선택해 시작한 회기가 `POST /sessions` payload와 DB-backed `GET /sessions/{id}` 상세의 `theory_mode=cbt`로 보존되는지 검증한다.
- 2026-07-01 focused 검증: `PLAYWRIGHT_PORT=5231 npx playwright test e2e/voice.spec.ts --project=chromium-single-run --workers=1 --grep "consent withdrawal"` **1 passed**. 동의 철회 뒤 이미 생성된 `session_id``/api/voice/ws`를 열어도 `consent_required`로 닫히는지 실제 브라우저 WebSocket으로 검증한다.
- 2026-07-01 focused 검증: `py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_dataset_export.py app/test_phase3_artifact_checker.py -q` **16 passed**. X1 exporter가 consented/client-visible `text_masked` 턴만 고르고 raw `sc.text`를 선택하지 않는지, supervisor comment raw text를 JSONL에서 제거하는지, approved/dry-run JSONL shape·row count·privacy·PII gate가 `{}` false-positive를 막는지 검증한다.
- 2026-07-01 focused 검증: `py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_evaluation_persistence.py app/test_session_turn_persistence.py app/test_live_coach_privacy.py -q` **50 passed**. H4 live coach prompt가 recent turns와 fast-loop evaluation dict의 문자열 leaf를 다시 마스킹하고, turn evaluation persistence가 rationale/comment/alternative utterance의 raw 이름·기관·전화번호를 DB insert 전 제거하며, legacy DB row의 빈 `text_masked` fallback과 session-end deep evaluation payload/read-model도 raw PII를 재마스킹하는지 검증한다. 같은 라운드에서 `test_evaluation_persistence.py``IsolatedAsyncioTestCase`로 바꿔 async persistence 테스트가 실제 await되도록 고정했다.
- 2026-07-01 추가 backend focused 검증: `py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_session_turn_persistence.py -k "fast_loop_evaluation" -q` **2 passed** + `py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_learner_dashboard.py -k "failed_fast_loop" -q` **1 passed**. fast-loop 평가 훅 예외는 상담 턴 저장을 막지 않되 `evaluation.error`와 리뷰의 `턴 평가 실패` 노트로 표면화하고, evaluator가 반환한 error dict는 성장 점수·최근 피드백에서 제외해 실패가 neutral 0.5로 집계되지 않게 검증한다.
- 2026-07-01 추가 backend focused 검증: `py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_eval_routes.py app/test_evaluation_persistence.py -q` **29 passed**. 단일 턴 `reevaluate_turn`이 결과를 응답 바디에만 두지 않고 `feedback_scores`/`turn_technique`/`turn_client_state`/`supervisor_comment`/`alternative_utterance` row를 교체 저장하며, 저장 실패는 503, 평가 error는 저장 후 502/503으로 표면화하는지 검증한다. normalized evaluation table의 delete RLS policy도 함께 고정한다.
- 2026-07-01 추가 backend focused 검증: `py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_session_turn_persistence.py -k "session_evaluation or stale_missing" -q` **3 passed**. 오래된 종료 회기가 `app.session_evaluation` row 없이 무한 `평가 대기`로 남지 않고, timeout+grace 이후 교수자 read-model에서 `평가 실패``AI 평가 재시도가 필요합니다` 사유로 표면화되는지 검증한다.
- 2026-07-01 추가 backend focused 검증: `py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_notifications.py app/test_session_turn_persistence.py app/test_evaluation_persistence.py -k "session_evaluation or missing_session_evaluation or scheduled_session_evaluation" -q` **16 passed**. startup recovery가 종료됐지만 evaluation row가 없는 오래된 DB 세션을 evaluator AI context로 찾아 기존 session-end 평가를 재예약하고, 같은 session_id 중복 background 평가를 process-local in-flight set으로 막는지 검증한다.
- 2026-07-01 focused 검증: `PLAYWRIGHT_PORT=5228 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1 --grep "case worksheet|crisis safety"` **2 passed**. C1 학습자 워크시트 저장→교수자 수정요청 검수와 C2 위기 신호→`app.safety_events`→DB-backed 교수자 안전 알림 큐를 실제 브라우저/API/DB로 검증한다.
- 2026-07-01 추가 DB-backed 검증: `PLAYWRIGHT_PORT=5244 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1 --grep "Korean PII|crisis safety event"` **2 passed**. H4는 실제 Session UI stream으로 한국어 이름·기관·전화번호가 포함된 학습자 발화를 보내고, DB-backed `GET /sessions/{id}` 상세와 `/review` 모두 raw 값 없이 `[NAME]`/`[ORG]`/`[PHONE]`으로 마스킹되는지 검증한다. C2는 위기 발화가 교수자 검토용 learner turn으로 저장되지만 client AI 응답 turn은 저장되지 않고, DB-backed 교수자 안전 큐의 109 알림이 유지되는지 확인한다.
- 2026-07-01 focused 검증: `PLAYWRIGHT_PORT=5193 npx playwright test e2e/session-mvp.spec.ts --project=chromium-single-run --workers=1` **2 passed**. MVP 종료/리뷰 흐름과 위기 안전 게이트가 빈 내담자 응답 신호에 덮이지 않는지 검증한다.
- 2026-07-01 focused 검증: `PLAYWRIGHT_PORT=5215 npx playwright test e2e/teacher.spec.ts --project=chromium-single-run --workers=1` **9 passed**. 교수자 콘솔의 페르소나 검수, 검토 큐, 최근 회기 진입과 별도 `/teach/analysis` 학생 분석 메뉴, 학습자 검색 테이블, 행 펼침, 상세 드릴다운, 전체 회기 탭 전환을 검증한다.
- 2026-07-01 focused 검증: `PLAYWRIGHT_BASE_URL=http://localhost:5210 npx playwright test e2e/teacher.spec.ts --project=chromium-single-run --grep "shows selected learner analysis"` **1 passed** + `PLAYWRIGHT_BASE_URL=http://localhost:5210 npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run --grep "professor (student analysis overview|learner detail analysis)"` **2 passed**. 실제 5210 로컬 스택에서 학생 분석 목록의 사용자 표시명, 상세 기본 페르소나별 회기 탭, 페르소나 행 펼침, 전체 회기 탭 전환, 7개 폭 레이아웃 containment를 검증한다.
- 2026-07-01 focused 검증: `C:\Users\encep\AppData\Local\Programs\Python\Python311\python.exe -X utf8 -m pytest -p no:cacheprovider app/test_teacher_dashboard.py -q` **10 passed**. 교수자 대시보드 API는 `session_persistence.list_all_sessions()`로 전체 담당 세션을 읽고, 학습자 일반 목록은 `list_recent_sessions()`로 최근 100개 제한을 명시해 한 학생의 최신 회기가 다른 학생 분석 목록을 밀어내지 않는지 검증한다. 또한 ended session summary가 `app.session_evaluation``evaluation_status`, `review_ready`, `supervisor_state`, `evaluation_error`를 별도로 싣고, 평가 실패가 수동 review status와 섞이지 않는지 검증한다.
- 2026-07-01 focused 검증: `npx playwright test e2e/admin.spec.ts --project=chromium-desktop --project=chromium-mobile --workers=1 --grep "approved admin without onboarding"` **2 passed**. 승인된 `role=admin` 사용자의 세션에 `admin_access=false`, `onboarding_completed_at=null`이 남아도 `/admin``/onboarding`으로 우회하지 않고 관리 콘솔의 `/admin/*` API를 호출하는지 fixture로 고정한다.
레이아웃·시각 회귀 게이트(핵심 합격선):

View file

@ -1,140 +1,76 @@
# Vignette 백로그 — 2026-06-26
# Vignette 백로그 — 열린 항목만 (현행본)
이 세션(레이아웃 시각 수용 마감 + live 운영 증거 라운드) 이후 남은 항목을 한곳에 모은다.
대시보드(`docs/dev_dashboard.html`)의 상태와 일치한다. 운영 원칙상 가짜 증거로 DONE 표기하지 않으므로,
실증/외부 의존/소유자 결정이 필요한 항목은 여기서 추적한다.
> **SSOT는 `docs/dev_dashboard.html`이다.** 상태·검증 증거·결정·로드맵의 권위 기준은 대시보드다.
> 이 파일은 대시보드와 일치하는 **아직 열린 작업(외부/환경 게이트)** 만 얇게 유지한다.
> 완료된 작업의 변경 로그·검증 기록(구 "최신 동기화 추가"·"이번 세션에 닫은 것"·B1~B3 `[x]` 상세)은
> `docs/archive/ops/backlog-2026-06-26-full-history.md`로 냉동 보관했다. 필요할 때만 이력용으로 본다.
>
> 운영 원칙: 가짜 증거로 DONE 표기 금지. 실증 불가·외부 의존·소유자 결정 항목만 여기서 추적한다.
> 분류: **B1 비차단 폴리시** · **B2 환경 제약(증거 생산 불가)** · **B3 소유자 결정** · **B4 외부 거버넌스**
최신 동기화(2026-06-29): M2 다회기 케이스 아크는 `TurnMemory` 값 객체로 턴 메모리 전달 경계를 줄였고, `DigestQualityAssessment`/`SessionDigestWorkerOutcome`로 LLM digest 후보 local quality harness를 추가했다. 이번 라운드에서는 `session_digest_worker.py``CompressionJob`→Node-compatible `GenerateRequest` 변환, 주입형 engine/audit 호출, accepted-only `session_summary`/`case_profile` 적용 계획과 one-shot DB loader 경계를 소유한다. `scripts/run-session-digest-worker.py`는 metadata-only dry-run/apply runner를 제공하고, API는 `SESSION_DIGEST_WORKER_ENABLED=false` 기본값에서 opt-in일 때만 세션 종료 뒤 background worker를 예약한다. loader/apply는 `compressed_by IS NULL` CAS로 이미 압축된 세션 재실행 race를 막고, engine 호출은 DB transaction 밖에서 수행한다. loader는 persisted fallback summary와 client-visible `text_masked` transcript만 재구성하며 raw `text`, evaluator-only turn, CCD, end_state를 압축 prompt에 넣지 않는다. `scripts/check-dev-dashboard-ssot.py`는 대시보드 카드 상태와 M2 30/87 검증 수치, stale DONE/GATE 문구를 guard한다. `recall_summary`/`pinned_facts`/`recent_turns`/`kb_behavior_cues``TurnContext.memory`에 보관되고, `prepare_turn(memory=...)`에서 마스킹된 뒤 `build_turn_messages(memory=...)`로 전달된다. auth managed-user upsert 입력도 `ManagedUserUpsertInput`으로 단일화했지만, 이는 내부 호출 경계 정리이며 외부 API·온보딩 정책·권한 모델 변경은 아니다. Python engine gateway의 `provider/model` 응답 메타 규칙은 helper로 모았고, `GatewayPromptParts`가 current-turn `system_prompt`/`user_payload` 분해 경계를 소유한다. Node conformance runner는 `gateway-default`가 request default-routing sentinel이며 response/done model은 resolved provider model이어야 함을 검증한다. 검증은 `py_compile`, runner `--help`, dashboard SSOT checker PASS, M2 digest worker + memory focused 30 passed, M2 주변 회귀 87 passed, M2 local harness 20 passed, M2/voice/evaluation focused 115 passed, auth/admin/session focused 103 passed, gateway contract 27 passed, backend focused 120 passed, Node conformance OK, `npm run check:api-types`. 단, 실 provider 장시간 운영·임상 골든셋 품질평가·재압축은 계속 후속 GATE다.
최신 동기화 추가(2026-06-29): 관리자·교수자 메일링 시스템 1차가 추가됐다. 가입 승인 요청과 회기 검토 요청은 `app.notification_event`/`app.notification_delivery`에 idempotent 큐로 남기고, SMTP 설정이 있을 때만 실제 메일을 발송한다. 메일 본문은 Vignette 톤앤매너를 따르되 축어록·평가 전문을 포함하지 않고 `/admin/users`, `/teach/session/:sessionId/review` 딥링크만 제공한다.
최신 동기화 추가(2026-06-30): 기본 역할이 learner로 복원되는 슈퍼관리자 계정이 타 학습자 회기 리뷰 딥링크에서 `session does not belong to user` 403에 막히던 문제를 수정했다. 리뷰 조회는 본인 학습자 접근을 우선 유지하고, 실패 시 슈퍼관리자만 admin 감독자 읽기로 재시도한다. 프론트는 동일 403뿐 아니라 이미 `teacherReview`가 포함된 성공 응답도 감독자 권한 사용자에 한해 `/teach/session/:sessionId/review`로 정규화한다. 일반 learner 및 단순 `admin_access` 위임 계정의 타인 세션 접근 차단은 유지했다. 검증: `app/test_rbac_idor.py` 11 passed, RBAC/auth/teacher focused 47 passed, supervisor route E2E 2 passed(`PLAYWRIGHT_PORT=5176`), `npm run typecheck`, `npm run build`.
최신 동기화 추가(2026-06-30): `docs/dev_dashboard.html` 자체를 레드팀 검증 대상으로 추가했다. `scripts/test_dev_dashboard_ssot.py`는 SSOT checker import/stateless/owner parser를 unit으로 검증하고, `apps/web/e2e/dev-dashboard.spec.ts`는 local file, `127.0.0.1`, `localhost` origin에서 필터 오순서·빠른 재시도·키보드·details/tabs·hash anchor·링크/이미지 무결성·desktop/mobile overflow를 E2E로 확인한다. 같은 라운드의 라이브 점검에서 public API `https://api-vignette.chanpaca.net/health``/personas`가 Cloudflare 530/error code 1033으로 실패하는 것을 발견했다. 원인은 Docker Desktop 중단으로 `vignette-dev-db`가 꺼진 상태였고, Docker Desktop/DB 재기동 뒤 `scripts/start-public-runtime.ps1`를 다시 실행해 public health 200, unauth `/personas` 401, auth config 200, Google login start 302, watchdog healthy: engine, api, web-preview, cloudflared, public-api까지 복구했다. 검증: dev-dashboard E2E redteam 10 passed, SSOT unit 3 OK, dashboard checker PASS.
최신 동기화 추가(2026-07-01): 하린/P6 종료 회기의 `SUPERVISOR AI 평가 실패`는 평가가 실행되지 않은 문제가 아니라 102분/138턴 축어록에 대한 회기말 평가 background task가 기존 45초 하드캡에서 timeout된 뒤 `TimeoutError`의 빈 문자열이 그대로 저장된 문제였다. 회기말 평가는 `SESSION_EVALUATION_TIMEOUT` 또는 `ENGINE_TIMEOUT`을 사용하고, 빈 예외 문자열은 타입/명시 메시지로 저장한다. 교수자 재평가 API는 실패 결과를 저장한 뒤 502/503으로 노출하고, 프론트는 종료 실패 alert, 평가 대기 polling, 교수자 `AI 평가 재시도` 버튼을 제공한다. 재시도 실패가 UI에서 조용히 사라지지 않고 기존 실패 리뷰와 새 실패 사유를 유지하는지, 저장된 평가 실패 row가 read-model에서 `평가 실패`와 사유로 노출되는지도 회귀 테스트로 고정했다. 모바일 코칭 모드에서 우측 컬럼 숨김 때문에 AI 튜터 카드가 보이지 않던 회귀도 수정했다. 검증: `python -X utf8 -m pytest app/ -q` 297 passed, `npm run check:api-types`, `npm run typecheck`, `npm run build`, `session-review` desktop/mobile 16 passed, `session-mvp` desktop/mobile/single-run 5 passed, `session-layout` desktop/mobile 8 passed, `layout-visual-gate` 9 passed.
최신 동기화 추가(2026-07-01): 실제 브라우저 SSE stream 저장 경로를 E2E로 추가 검증했다. 기존 `session-persistence.spec.ts`는 real API/DB를 쓰지만 `/api/sessions/{id}/turn` 직접 호출이라 UI의 `openSessionStream()``/sessions/{id}/stream` → 저장 finalize 경로를 타지 않았다. 새 `@single-run` 테스트는 `db=true`, `engine=true` health 조건에서 Session UI로 회기 시작, 학습자 발화 전송, stream 완료 대기, DB-backed `/review` learner/client 축어록 확인까지 수행한다. 이어서 AI 튜터 코칭 카드가 생성만 되고 `app.live_coach_events` 저장 실패가 묻히는 silent failure를 막기 위해 `POST /sessions/{id}/live-coach`의 저장 실패를 503으로 표면화하고, 실제 UI 코칭 모드 → DB-backed `/live-coach` 이력 → 재로딩 후 `C` 마커 유지까지 E2E로 고정했다. crisis `safety`/`done.conversation_stopped` 또는 음성 `reply.conversation_stopped`도 빈 응답으로 일반화하지 않고 109 안전 게이트를 유지하도록 수정했다. 검증: `python -X utf8 -m pytest -p no:cacheprovider app/test_session_turn_persistence.py -q` 27 passed, `PLAYWRIGHT_PORT=5193 npx playwright test e2e/session-mvp.spec.ts --project=chromium-single-run --workers=1` 2 passed, 최신 `PLAYWRIGHT_PORT=5202 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1` 6 passed, `npm run typecheck`, `npx playwright test --list` 현재 160 tests / 18 files.
최신 동기화 추가(2026-07-01): 음성 비언어 메타데이터도 mock/fixture 전용 검증에 머물던 구간을 좁혔다. `db.healthcheck()`, runtime table readiness, deploy preflight DB mode가 `app.turns.provider_events` 하나가 아니라 `audio_ref`/`silence_ms`/`speech_rate`/`barge_in`/`provider_events` 5개 컬럼 전체를 확인한다. dev/runtime bootstrap은 누락된 nullable 음성 컬럼도 보강한다. `session-persistence.spec.ts`에는 실제 DB 회기를 만들고 `/api/voice/ws` `stt_result` control 및 실제 Session 마이크 UI 생성 메타를 DB-backed `/review` 제한 `nonverbal` 칩까지 검증하는 `@single-run` E2E를 추가했다. 검증: `python -X utf8 -m pytest -p no:cacheprovider app/test_runtime_policy.py app/test_session_turn_persistence.py app/test_voice_ws.py app/test_voice_service.py -q` 77 passed, `PLAYWRIGHT_PORT=5199 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1` 5 passed, `python -X utf8 -m py_compile app/db.py app/auth_sessions.py app/test_runtime_policy.py ..\..\scripts\check-deploy-preflight.py`, `python -X utf8 scripts\check-deploy-preflight.py --skip-db --env-file infra\.env.example --allow-placeholder-secrets`.
최신 동기화 추가(2026-07-01): 실제 Session 마이크 UI 경로도 음성 비언어 메타를 생성하도록 보강했다. `AudioWorklet` 캡처는 PCM RMS/peak 기반으로 voice activity와 trailing silence를 추정하고 `audio_end``silence_ms`, `barge_in=false`, `provider_events`(`voice_activity`, `silence`)를 싣는다. `MediaRecorder` fallback은 raw PCM을 볼 수 없으므로 녹음 duration 기반 voice activity만 보낸다. 리뷰 read-model은 turn-level `silence_ms`가 이미 침묵 칩을 만들면 provider `silence` 칩을 중복 노출하지 않는다. `voice-success.spec.ts`는 실제 마이크 버튼 경로가 `audio_end` 메타를 보내는지 확인하고, `session-persistence.spec.ts`는 그 UI 생성 메타가 DB-backed `/review` `nonverbal`까지 이어지는지 확인한다. 검증: `npm run typecheck`, `python -X utf8 -m pytest -p no:cacheprovider app/test_runtime_policy.py app/test_session_turn_persistence.py app/test_voice_ws.py app/test_voice_service.py -q` 77 passed, `PLAYWRIGHT_PORT=5197 npx playwright test e2e/voice-success.spec.ts --project=chromium-single-run --workers=1` 2 passed, 최신 `PLAYWRIGHT_PORT=5202 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1` 6 passed, `npx playwright test --list` 현재 160 tests / 18 files.
최신 동기화 추가(2026-07-01): 회기 종료 후 background deep 평가가 실제 durable DB row로 저장되는지 fixture 없이 검증했다. `_schedule_session_evaluation()`은 생성 task를 반환하고 done callback에서 crash/cancel을 관찰하며, 평가 결과·timeout/error 저장이 durable store에 닿지 않으면 명시 로그를 남긴다. 교수자 명시 재평가 API는 평가 산출물이 생성됐지만 저장 실패하면 503으로 노출하고, `GET /eval/sessions/{id}/evaluation``status/error/durable`을 제공해 캐시 복원과 DB 저장 증거를 구분한다. `session-persistence.spec.ts`는 학습자 세션 생성→턴→종료→teacher 로그인→`/eval/.../evaluation` polling→교수자 리뷰 UI `평가 완료`까지 실제 DB/API/엔진으로 확인한다. 검증: `python -X utf8 -m pytest -p no:cacheprovider app/ -q` 309 passed, `npm run check:api-types`, `npm run typecheck`, `PLAYWRIGHT_PORT=5205 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1` 7 passed, `PLAYWRIGHT_PORT=5203 npx playwright test e2e/session-review.spec.ts --project=chromium-desktop --project=chromium-mobile --workers=1` 16 passed, `npx playwright test --list` 현재 162 tests / 18 files.
최신 동기화 추가(2026-07-01): 교수자 회기 리뷰 데스크톱에서 `overview`/`transcript`가 여러 CSS Grid row를 가로질러 오른쪽 검토 rail 높이를 나눠 먹으며 중앙에 큰 빈 row가 생기던 문제를 제거했다. 1181px 이상 교수자 리뷰는 main column + review rail 2열 wrapper로 분리하고, main 내부는 요약 전체폭, 차트/회기 흐름 2열, 축어록 전체폭으로 배치한다. 회귀 방지로 `layout-visual-gate.spec.ts`에 교수자 리뷰 폭별 dead vertical gap 검증을 추가했다. 검증: `npm run typecheck`, `npm run build`, `npx playwright test e2e/session-review.spec.ts --project=chromium-desktop --workers=1` 8 passed, `npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run --workers=1` 10 passed, `npx playwright test e2e/session-layout.spec.ts --project=chromium-desktop --project=chromium-mobile --workers=1` 8 passed.
최신 동기화 추가(2026-07-01): 교수자 학생 분석을 콘솔에서 분리했다. `GET /teacher/learners/{learner_id}/analysis`는 담당 범위 안의 특정 학습자 전체 회기를 오래된 순서로 반환하고, 제한 없는 회기별 추이 point, 라포·탐색·개입·정리 단계 분포, 검토 대기/완료 카운트를 함께 내려준다. `/teach`는 검토 큐·위기 알림·페르소나 검수·최근 회기 triage만 맡고, 좌측 메뉴의 `/teach/analysis`는 전체 학습자 검색 테이블 → 우측 행 펼침 요약 → `?learner=` 사용자별 상세 → 추이/전체 회기/단계 분석 탭 → 회기 리뷰 드릴다운을 맡는다. 검증: `python -X utf8 -m pytest -p no:cacheprovider app/test_teacher_dashboard.py -q` 6 passed, `npm run check:api-types`, `npm run typecheck`, `npm run build`, `PLAYWRIGHT_PORT=5215 npx playwright test e2e/teacher.spec.ts --project=chromium-single-run --workers=1` 9 passed, `PLAYWRIGHT_PORT=5217 npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run --workers=1` 12 passed, `npx playwright test --list` 현재 166 tests / 18 files.
최신 동기화 추가(2026-07-01): 원천문서 갭 E2E 탐색을 이어서 H1/C3 silent failure 후보를 좁혔다. `session-persistence.spec.ts`는 Phase 3 pre/post 3척도 입력이 브라우저 리뷰 UI → `PUT /users/me/prepost-measures` → DB-backed `GET /users/me/prepost-measures` → 새로고침 후 UI 복원까지 실제 API/DB로 이어지는지 검증한다. C3 수동 재평가는 persona 기본 `theory_target`이 학습자가 선택한 session `theory_mode`를 덮어쓰던 우선순위 오류를 수정했고, turn 재평가도 `TurnContext.theory_mode`를 채우도록 회귀화했다. 서브에이전트 병렬 조사 결과 남은 큰 공백은 C1 워크시트 learner-save/teacher-review full browser+API+DB E2E, C2 safety_events→교수자 대시보드 E2E, H2 live-coach evidence modal/source-pack actual sync proof였다. 검증: `python -X utf8 -m pytest -p no:cacheprovider app/test_eval_routes.py app/test_notifications.py app/test_session_turn_persistence.py -q` 39 passed, `PLAYWRIGHT_PORT=5205 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1` 7 passed, `npx playwright test --list` 현재 162 tests / 18 files.
최신 동기화 추가(2026-07-01): 위 문단의 C1/C2 E2E 공백을 실제 브라우저/API/DB 검증으로 닫았다. C1은 학습자 사례개념화 워크시트 저장 → 교수자 검토 화면 read-only 복원 → `수정요청` 검수 메모 저장/재조회까지 `session-persistence.spec.ts`에 추가했다. C2는 `app.safety_events`가 runtime bootstrap/healthcheck에서 빠져 DB 정상처럼 보이던 readiness 공백과, `jsonb` codec 위에 이미 직렬화한 문자열을 넘겨 detail이 JSON string으로 저장되던 사일런트 오류를 수정했다. `record_safety_event()`는 RLS용 `ai_context=true`로 저장하고, 안전 알림 list 실패는 non-dev에서 fail-closed한다. 남은 큰 공백은 H2 live-coach evidence modal/source-pack actual sync proof다. 검증: `python -X utf8 -m pytest -p no:cacheprovider app/test_session_turn_persistence.py app/test_teacher_dashboard.py app/test_runtime_policy.py -q` 59 passed, `PLAYWRIGHT_PORT=5228 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1 --grep "case worksheet|crisis safety"` 2 passed, `npx playwright test --list` 현재 166 tests / 18 files.
분류: **B1 비차단 폴리시** · **B2 환경 제약(증거 생산 불가)** · **B3 소유자 결정** · **B4 외부 거버넌스**
> **B0. 원천문서 갭 분석 (2026-06-26 추가)** — 한신대 산학협력 원천문서 5종 정독으로 도출한 "부족한 부분"(critical 3 / high 4 / medium+ 6)은 **SSOT 대시보드** `docs/dev_dashboard.html` "원천문서 갭 분석" 섹션과 상세 `docs/ops/source-docs-gap-analysis-2026-06-26.md`에서 추적한다. C1 사례개념화 산출물은 저장형 워크시트, 외부 루브릭 scaffold, 교수자 수동 검수 상태 저장까지 4차 구조를 만들었고, C2 위기개입 프로토콜은 1차 구조, C3 이론모드는 2차 명시 선택 UI까지 만들었다. 콘텐츠 정의는 임상팀(구훈정·어유경) 소유라 코드는 구조를 선제 구축하되 임상 문안과 평가기준은 외부 정의로 받는다.
> **B0. 원천문서 갭** — 한신대 산학협력 원천문서 갭(critical/high/medium)은 SSOT 대시보드 "원천문서 갭 분석"
> 섹션과 상세 근거 `docs/ops/source-docs-gap-analysis-2026-06-26.md`에서 추적한다. C1~C3/H·M 구조는 코드로
> 선제 구축했고, 임상 문안·평가기준·골든셋 콘텐츠는 임상팀(구훈정·어유경) 외부 정의로 받는다.
---
## B1. 비차단 코스메틱 폴리시
## B1. 비차단 코스메틱/후속 폴리시
- [x] **공용 셸 사이드바 구분선 전체 높이** — (2026-06-26 처리) `.vg-nav border-right`(sticky·뷰포트 높이)를 제거하고, `.vg-shell__body` 컨테이너 배경 하어라인(`linear-gradient` 1px×100%, `--nav-cur`로 그리드·구분선 폭 동기화)으로 본문 그리드 전체 높이를 따라가는 세로 구분선을 그린다. 1024 축소폭은 `--nav-cur: var(--nav-w-collapsed)`로, 720 이하(상단 가로바 전환)는 `background-image:none`으로 정합. `components/shell/shell.css`. **검증: `npm run typecheck` PASS + `vite build` PASS.** 시각 회귀 게이트(gate 7/7 + 포커스 E2E + session-layout 8/8)는 web+api+DB 스택(Docker 미기동·secrets 부재)으로 이 환경 미실행 — 스택 가용 시 1회 시각 확인 권장.
- [x] **세션 종료 UX와 dark theme SSOT** — (2026-06-27 처리) 오발동과 모바일 조작 불안을 줄이기 위해 세션 하단의 드래그형 `SlideToEnd`를 명시 확인 다이얼로그로 교체했다. Topbar/Settings의 `vignette.theme` 읽기·저장·`data-theme` 적용은 `apps/web/src/lib/theme.ts` 단일 경로로 통합했고, 저장값이 없으면 dark 기본값을 부팅 전에 적용한다. API 기본 preference `system`은 Settings에서 light로 오해하지 않고 현재 초기 테마를 따른다. 회기 리뷰·설정 포함 주요 페이지의 흰 섹션 잔재도 dark 작업면으로 맞췄다. **검증: `npm run typecheck`, `npm run build`, `npx playwright test e2e/layout-visual-gate.spec.ts e2e/session-layout.spec.ts e2e/avatar-expression.spec.ts e2e/session-review.spec.ts e2e/settings.spec.ts --project=chromium-desktop --project=chromium-mobile --project=chromium-single-run --workers=1` 36 passed. 추가 시각 검사: 7개 주요 화면 x 3뷰포트 캡처 21장, horizontal overflow 0, 큰 흰 CSS 배경면 0.**
- [x] **P1 세션 아바타 PSD v2 정렬** — (2026-06-27 처리) 컨셉 보드 크롭 `p1-concept`를 기본값에서 내리고, 사용자 제공 `라투디 여캐_ver2.psd`에서 추출한 `seoyeon-live2d-psd-v2`를 P1 기본값으로 연결했다. `sad` 표정은 PSD의 울상 눈썹, 우는 입, 눈물 파츠를 별도로 합성하고, 데스크톱 스테이지는 3행 구조를 유지해 현재 상태 배지가 아바타와 겹치지 않게 했다. **검증: PSD 파츠 추출 53개, `rasterArtSet="seoyeon-live2d-psd-v2"`, sad 프리뷰에서 눈물/우는 입/울상 눈썹 확인.**
- [x] **SEO/GEO 기본 신호 + 회기 리뷰 URL 공유 카드** — (2026-06-27 처리) `index.html`에 canonical/description/Open Graph/Twitter Card/JSON-LD를 추가하고, `robots.txt`/`sitemap.xml`/`llms.txt`를 게시했다. 종료된 학습자 회기는 `POST /sessions/{id}/share`가 공개 토큰을 생성하고 `GET /share/session/{token}`이 서버 HTML로 요약·썸네일·JSON-LD를 내려 URL unfurl을 지원한다. `app.session_share_link`에는 토큰 해시와 sanitized preview payload만 저장하며 원문 축어록·학습자 식별자는 포함하지 않는다. 공유 페이지는 교수자 전달용이라 `noindex`를 유지한다. **검증: `pytest app/test_session_share.py app/test_session_turn_persistence.py -q` 21 passed, `python -m pytest app/ -q` 178 passed, `python -m pytest engine_gateway/ -q` 11 passed, `npm run check:api-types`, `npm run typecheck`, `npm run build`, `PLAYWRIGHT_PORT=5174 npm run e2e` 113 passed.** 배포 후 실제 카카오톡/Slack/메일 unfurl 1회 확인 권장.
- [x] **학습자·교수자·운영·페르소나 스튜디오 레이아웃 정렬** — (2026-06-28 처리) 작업형 화면에만 `AppShell wide` 폭 정책을 적용하고, 학습자 홈 회기 목록은 큐형 행으로 정리했다. 교수 콘솔은 검토 대기·위기 알림·페르소나 검수·페르소나 저작실 순서가 DOM과 화면 모두 일치하며, 운영 콘솔 사용자/티켓은 조밀한 운영 행으로 낮췄다. 페르소나 스튜디오는 SSOT 첨부/RAG 생성/항목 편집/검증 큐를 한 작업면에서 유지한다. 리포트와 대표 캡처: `docs/ops/layout-research-2026-06-28/dashboard-layout-alignment-report.md`, `docs/ops/layout-research-2026-06-28/final-gate-*.png`. **검증: `npm run typecheck`, `npm run build`, `npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run --workers=1` 7 passed, `npx playwright test e2e/session-layout.spec.ts --project=chromium-desktop --project=chromium-mobile --workers=1` 8 passed, `npx playwright test e2e/learner.spec.ts e2e/teacher.spec.ts e2e/admin.spec.ts --project=chromium-desktop --workers=1` 17 passed.**
- [x] **관리자 권한 위임 1차** — (2026-06-28 처리, 2026-06-29 보강) 역할(role)과 관리자 페이지 진입권(`app_user.admin_access`)을 분리했다. `AUTH_SUPER_ADMIN_EMAILS` 기본값은 `yunchan@twentyoz.kr`, `hoonjungkoo@hs.ac.kr`이며 슈퍼 관리자만 `/admin/users`에서 관리자 권한을 부여·회수한다. 실제 `admin` 역할 사용자는 학습자·교수자·관리자 라우트 전환이 가능하고, `/admin` 왼쪽 메뉴도 운영 홈/사용자/권한/티켓과 교수자·학습자 작업 공간을 함께 노출한다. 일반 학생/교수 계정은 `admin_access=true`여도 관리자 콘솔 진입권만 얻고 학습자·교수자 역할권은 추가되지 않는다. **검증: `python -B -m pytest -p no:cacheprovider app/test_auth_providers.py app/test_runtime_policy.py app/test_rbac_idor.py app/test_teacher_dashboard.py app/test_learner_dashboard.py -q` 70 passed, `python -B -m pytest -p no:cacheprovider app/test_auth_providers.py app/test_admin_ops.py app/test_runtime_policy.py -q` 64 passed, `npm run check:api-types`, `npm run typecheck`, `npm run build`, `npx playwright test e2e/admin.spec.ts --project=chromium-desktop --workers=1` 8 passed.**
- [x] **관리자·교수자 메일링 시스템 1차** — (2026-06-29 처리) 신규 외부 로그인 사용자가 `account_status=pending`이면 관리자 승인 메일 큐를 만들고, 회기 종료 후 평가 또는 error record가 저장되면 담당 교수자/관리자에게 회기 검토 메일 큐를 만든다. `app.notification_event``account_pending_approval:{user_id}`, `session_review_ready:{session_id}`, `admin_test_email:{uuid}` idempotency key로 중복 발송을 막고, `app.notification_delivery`는 수신자별 `queued/sending/sent/failed/skipped` 상태와 재시도 정보를 보관한다. 테스트 메일은 대량 오발송을 피하기 위해 `AUTH_SUPER_ADMIN_EMAILS` 대상에게만 큐잉한다. SMTP 설정은 `NOTIFICATION_EMAIL_PROVIDER=smtp`, `SMTP_*` env로 주입하며, 관리자 API `GET /admin/notifications`, `POST /admin/notifications/process`, `POST /admin/notifications/test``scripts/run-notification-worker.py`로 큐를 확인·처리한다. HTML 메일은 Vignette 종이 배경/세이지 CTA 톤을 inline style로 유지하고, 본문에는 축어록·평가 전문을 넣지 않는다. **검증: `python -m compileall apps\api\app\services\notifications.py apps\api\app\routes\admin.py apps\api\app\routes\sessions.py apps\api\app\auth_sessions.py apps\api\app\routes\users.py`, `python -B -m pytest -p no:cacheprovider app/test_notifications.py app/test_auth_providers.py app/test_admin_ops.py app/test_runtime_policy.py -q` 69 passed, `npm run generate:api-types`, `npm run check:api-types`, `npm run typecheck`, `npm run build`, `python scripts\check-deploy-preflight.py --env-file infra\.env.example --allow-placeholder-secrets --skip-db`, `python scripts\run-notification-worker.py --help`, `python -X utf8 scripts\check-dev-dashboard-ssot.py --json` PASS.**
- [x] **회기 아카이브 저장/복원 API** — (2026-06-28 처리) `/learn/history``보관됨`을 실제 학습자별 보기 상태로 연결했다. 종료 회기는 `POST /sessions/{id}/archive`/`restore`로 보관·복원하고, `app.session_archive_state`는 삭제가 아니라 `archived_at`/`updated_at`만 저장한다. `LearnerSessionSummary.archived`, `SessionArchiveResponse`, `LearnerDashboardOverview.archived_sessions`를 OpenAPI에 고정했고, 보관된 회기는 리뷰 대기 행동 큐에서 빠진다. 회기·턴·리뷰·공유 링크·연구/감사 증거는 삭제하지 않는다. **검증: `python -B -m pytest app/ -q` 178 passed, `python -B -m pytest engine_gateway/ -q` 11 passed, `npm run check:api-types`, `npm run typecheck`, `npm run build`, `npx playwright test e2e/learner.spec.ts --project=chromium-desktop --workers=1` 6 passed.**
- [x] **OpenAI TTS voice preset DB map 연결** — (2026-06-28 처리) `app.persona_voice_map`의 OpenAI row를 `/voice/ws` TTS voice 선택에 연결했다. 명시 query preset은 DB map보다 우선하고, 세션 바인딩은 `app.sessions.persona_id/persona_version`으로 voice map을 찾으며, dev persona 생성은 catalog persona id/version map을 사용한다. seed materializer는 기본 OpenAI voice map을 `ON CONFLICT DO NOTHING`으로 생성하고, dev 런타임 스키마 보강은 기존 DB의 `persona_voice_map` 누락도 복구한다. OpenAI가 아닌 provider row는 기존 persona-code fallback으로 안전하게 흡수한다. **검증: `pytest app/test_runtime_policy.py app/test_persona_review.py app/test_voice_service.py app/test_voice_ws.py -q` 75 passed, `pytest app/ -q` 178 passed, `pytest engine_gateway/ -q` 11 passed.**
- [x] **빈상태 컬럼 높이 여백** — (2026-06-28 처리) session-review 0건 상태는 1280px 이상에서 3컬럼 masonry를 쓰지 않고 빈 리뷰 전용 2컬럼 순차 레이아웃으로 전환한다. 가짜 기록·장식 콘텐츠를 넣지 않고, `EMPTY_REVIEW_SESSION_ID` fixture와 `session-review-empty` visual gate로 390/720/861/900/1024/1280/1440 전 폭을 검증한다. session-prestart와 learner-home은 기존 gate 대상에 남겨 함께 무회귀 확인했다. **검증: `npm run typecheck`, `npm run build`, `npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run --workers=1` 9 passed, `npx playwright test e2e/session-layout.spec.ts e2e/session-review.spec.ts --project=chromium-desktop --workers=1` 6 passed.**
- [x] **다크 UI v2 시안 실제 적용 라운드** — (2026-06-29 처리) `02/03/04-*-v2-dark-unified.png` 시안을 실제 React/CSS 화면으로 반영했다. P1 라이브 상담 세션은 내담자 무대 발화, 좌측 세션 진행, 우측 라이브 신호/안전 점검을 다크 훈련 화면으로 재구성했다. P2 회기 리뷰는 learner filled 상태를 3열 workbench(요약/축어록/평가 rail + 하단 워크시트)로 바꾸고 empty review는 sparse third column 없이 유지했다. P3 학습자 홈은 대시보드에서 연습 대상 레일을 제거하고, 진행 회기/리뷰 대기/최근 평가/라포 흐름을 상단 CTA 바로 올렸으며, 오늘 이어갈 회기·AI 코치·다음 연습 추천·최근 피드백을 하나의 `lh-work-cluster` 작업 묶음으로 재배치했다. 오른쪽 레일은 최근 기록, 리뷰 대기, 반복 대상만 담당한다. 생성 PNG는 앱에 직접 넣지 않고 레이아웃·밀도·톤 기준으로만 사용했다. 새 실제 렌더 캡처: `docs/design-verification/full-pages/02-learner-home-{desktop,tablet,mobile}.png`, `03-session-active-{desktop,tablet,mobile}.png`, `04-session-review-{desktop,tablet,mobile}.png`, `04-session-review-empty-{desktop,tablet,mobile}.png`. 실행 기록은 `docs/ops/dark-ui-refresh-implementation-plan-2026-06-28.md`에 고정했다. **최신 검증: `npm run typecheck`, `npm run build`, `npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run --workers=1` 9 passed.**
- [x] **세션 종료 평가 실패 복구 UX + 원인 저장** — (2026-07-01 처리) 긴 회기 종료 후 AI 평가가 45초 timeout으로 실패하면서 `error=""`만 저장되던 silent failure를 제거했다. 종료 실패는 리뷰 화면으로 넘어가지 않고 alert로 표시하며, 종료 후 리뷰는 `reviewReady=false` 상태에서 대기 polling으로 ready 전환을 기다린다. 교수자 검토 화면은 평가 실패/미준비 상태에서 `AI 평가 재시도` 버튼으로 `/eval/sessions/{id}/reevaluate`를 호출한다. 재시도 API는 실패 결과도 DB에 저장한 뒤 non-2xx로 노출한다. 재시도 실패 UI 유지와 저장된 평가 실패 read-model 노출도 회귀 테스트로 고정했다. background task crash 관찰과 저장 실패 표면화, `status/error/durable` 평가 조회, 실제 종료 후 durable DB 평가 row 생성 E2E까지 추가했다. 모바일 코칭 모드의 AI 튜터 카드 숨김 회귀도 함께 수정했다. **검증: `python -X utf8 -m pytest -p no:cacheprovider app/ -q` 309 passed, `npm run check:api-types`, `npm run typecheck`, `PLAYWRIGHT_PORT=5205 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1` 7 passed, `PLAYWRIGHT_PORT=5203 npx playwright test e2e/session-review.spec.ts --project=chromium-desktop --project=chromium-mobile --workers=1` 16 passed, 기존 `session-mvp`/layout focused gates 유지.**
- [x] **브라우저 SSE stream 저장 경로 회귀** — (2026-07-01 처리) `session-mvp` mock과 직접 `/turn` API 테스트 사이에 있던 실제 UI stream 저장 공백을 줄였다. `session-persistence.spec.ts`에 브라우저 Session UI가 실제 `POST /sessions/{id}/stream`을 호출하고, stream 완료 뒤 DB-backed `/review`에서 학습자/내담자 축어록이 반환되는지 검증하는 `@single-run` 테스트를 추가했다. 같은 파일에 AI 튜터 코칭 이력 저장/재로딩 회귀와 음성 비언어 메타 리뷰 회귀도 추가했다. **검증: `PLAYWRIGHT_PORT=5199 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1` 5 passed, `npm run typecheck`.**
- [x] **AI 튜터 코칭 이력 저장 실패 표면화** — (2026-07-01 처리) `POST /sessions/{id}/live-coach`가 코칭 제안 생성 후 `save_live_coach_event()` 실패를 `except/pass`로 묻어 카드만 보이고 이력/재로딩에는 사라질 수 있던 silent failure를 제거했다. 저장 계층의 prod fail-closed 503을 그대로 사용자/프론트에 노출하고, DB-backed `/live-coach` 이력과 학습자 발화 `C` 마커가 재로딩 뒤 유지되는지 E2E로 고정했다. **검증: `python -X utf8 -m pytest -p no:cacheprovider app/test_session_turn_persistence.py -k live_coach -q` 4 passed, `PLAYWRIGHT_PORT=5199 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1` 5 passed.**
- [x] **음성 비언어 메타 DB readiness + 리뷰 E2E** — (2026-07-01 처리) `/health`, runtime table readiness, deploy preflight가 `app.turns` 음성 메타 5개 컬럼 전체를 확인하게 했다. `session-persistence.spec.ts`는 DB-backed voice WS `stt_result`와 실제 Session 마이크 UI 생성 메타 → `/review` `nonverbal` 파생 칩까지 고정한다. 실제 provider 기반 한숨·울음 감지와 물리 마이크 장시간 WSS 실측은 계속 후속 gate다. **검증: backend focused 77 passed, `PLAYWRIGHT_PORT=5197 npx playwright test e2e/voice-success.spec.ts --project=chromium-single-run --workers=1` 2 passed, `PLAYWRIGHT_PORT=5199 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1` 5 passed, preflight skip-db PASS.**
- [x] **위기 안전 게이트 UI 덮어쓰기 방지** — (2026-07-01 처리) SSE/음성 위기 종료가 빈 client reply로 끝날 때 `내담자 응답 없음` 신호와 live-coach 호출이 안전 게이트를 덮던 경로를 막았다. `conversation_stopped`가 있으면 109 안전 자원, 입력 disabled, 위기 신호를 유지하고 코칭 요청을 보내지 않는다. **검증: `PLAYWRIGHT_PORT=5193 npx playwright test e2e/session-mvp.spec.ts --project=chromium-single-run --workers=1` 2 passed, `npm run typecheck`.**
- [x] **운영 티켓·헬스 샘플 이력 저장 모델 1차**`/admin` 운영 홈과 `/admin/tickets``app.admin_health_event`·`app.support_ticket` 실제 DB 모델에 연결됐다. `/users/support-tickets`는 인증 사용자의 문제·불만·장애 접수를 저장하고, `/admin/tickets`는 미해결 큐·우선순위·상태 변경을 처리한다. `/admin/uptime`은 상시 SLA가 아니라 관리자 콘솔이 관측한 헬스 샘플 정상률/중단 이력을 반환한다. 가짜 티켓 수·가짜 다운타임 숫자는 계속 금지. **검증: `python -m pytest app/test_admin_ops.py -q` 3 passed, `npm run generate:api-types`, `npm run typecheck`.**
- [x] **사용자별 운영 티켓 조회 UI** — (2026-06-28 처리) 학습자 Settings에 `지원 요청` 섹션을 추가해 본인이 접수한 티켓의 상태·카테고리·우선순위·담당 그룹·해결 메모를 읽기 전용으로 확인한다. `GET /users/support-tickets``reporter_id = current user` 기준으로만 조회하고, 본문과 신고자 이메일/이름은 응답·감사 검증에서 제외한다. **검증: `python -B -m pytest app/test_user_support_tickets.py app/test_admin_ops.py app/test_runtime_policy.py -q` 29 passed, `npm run generate:api-types`, `npm run check:api-types`, `npm run typecheck`, `npm run build`, `npx playwright test e2e/settings.spec.ts --project=chromium-desktop --workers=1` 7 passed, `npx playwright test e2e/admin.spec.ts --project=chromium-desktop --workers=1 --grep "operation tickets"` 1 passed.**
- [x] **상시 synthetic monitor/cron 샘플러 1차** — (2026-06-28 처리) `/admin/health`와 같은 헬스 계산을 `record_admin_health_sample()`로 공통화하고, 브라우저 세션 없이 `scripts/record-admin-health-sample.py``app.admin_health_event`에 서비스별 샘플을 append한다. Windows 예약 실행은 `scripts/install-health-sampler-task.ps1``VignetteAdminHealthSampler` Scheduled Task로 설치한다. 이 값은 SLA가 아니라 synthetic sampler가 관측한 최근 샘플 이력이다. **검증: `python -B -m py_compile apps/api/app/routes/admin.py apps/api/app/test_admin_ops.py scripts/record-admin-health-sample.py`, `python scripts/record-admin-health-sample.py --help`, `powershell -NoProfile -ExecutionPolicy Bypass -File scripts\install-health-sampler-task.ps1 -PrintOnly -IntervalMinutes 7`, `python -B -m pytest app/test_admin_ops.py app/test_runtime_policy.py app/test_user_support_tickets.py -q` 31 passed, 로컬 one-shot `recorded_count=5`, `status=ok`, `engine_mode=claude_cli`.**
- [x] **헬스 이벤트 retention/rollup 실행 경로 1차** — (2026-06-28 처리) raw `app.admin_health_event`와 일별 `app.admin_health_daily_rollup`을 분리했다. `scripts/maintain-admin-health-events.py``--rollup-days``--retention-days`를 명시해야 하며 기본은 dry-run이다. `--apply`는 rollup을 먼저 upsert한 뒤 retention window 밖 raw 샘플만 삭제하고, non-dev apply는 `--allow-non-dev-apply` 없이는 차단한다. `/admin/uptime` 요약은 raw 이벤트와 daily rollup을 중복 없이 합산하고, 상세 `events` 목록은 최근 raw 샘플만 유지한다. **검증: `python -B -m py_compile apps/api/app/db.py apps/api/app/auth_sessions.py apps/api/app/routes/admin.py apps/api/app/services/admin_health_maintenance.py scripts/maintain-admin-health-events.py`, `python scripts/maintain-admin-health-events.py --help`, `python -B -m pytest app/test_admin_health_maintenance.py app/test_admin_ops.py app/test_runtime_policy.py app/test_user_support_tickets.py -q` 36 passed, 로컬 dry-run `rollup_event_count=0`, `prunable_event_count=0`.**
- [x] **운영 티켓 필터·카테고리 큐·수동 처리 감사 1차** — (2026-06-28 처리) `/admin/tickets`가 status/category/priority/assigned_group/source_path/stale/search 필터를 서버 query로 처리하고, 운영 콘솔은 검색·상태·카테고리·우선순위·담당 그룹·정체 필터와 카테고리 quick queue를 제공한다. 티켓 접수와 관리자 수동 상태/우선순위/담당그룹/해결메모 변경은 `audit.audit_log`에 metadata-only(`support_ticket_create`, `support_ticket_update`)로 남기며, 제목/본문 전문은 감사 로그에 복제하지 않는다. **검증: `pytest app/test_admin_ops.py app/test_runtime_policy.py -q` 28 passed, `npm run generate:api-types`, `npm run check:api-types`, `npm run typecheck`.**
- [x] **운영 티켓 중복 저장·수동 연결 1차** — (2026-06-28 처리) `app.support_ticket`에 결정론적 `fingerprint``parent_ticket_id`를 추가했다. 사용자 티켓 생성은 fingerprint를 저장하지만 자동 병합·자동 담당그룹·자동 우선순위 변경은 하지 않는다. `/admin/tickets`는 같은 fingerprint의 중복 후보 수와 oldest parent 후보를 반환하고, 관리자는 기존 `PATCH /admin/tickets/{id}`로 parent 연결/해제를 수동 처리한다. self-parent와 parent cycle은 서버가 거부하며, 변경 감사는 `support_ticket_update` metadata-only로 남기고 subject/body 전문은 복제하지 않는다. Settings의 사용자별 티켓 조회에는 fingerprint/parent를 노출하지 않는다. **검증: `python -B -m py_compile app/auth_sessions.py app/routes/admin.py app/routes/users.py app/services/support_tickets.py`, `python -B -m pytest app/test_admin_health_maintenance.py app/test_admin_ops.py app/test_runtime_policy.py app/test_user_support_tickets.py -q` 38 passed, `npm run generate:api-types`, `npm run check:api-types`, `npm run typecheck`, `npm run build`, `npx playwright test e2e/admin.spec.ts --project=chromium-desktop --workers=1 --grep "operation tickets|duplicate"` 2 passed.**
- [ ] **운영 티켓 자동 분류·처리 후속** — Claude Recipe headless 자동 수정 후보, 관리자 승인 후 이슈 등록·PR/작업 스레드 생성, 처리 결과 audit trail 확장은 아직 설계/승인 필요. 담당 그룹 자동 배정·우선순위 escalation·raw/rollup 보존기간 같은 운영 정책은 B3 결정 항목에서 먼저 닫아야 한다. 자동 수정은 운영자 승인 전까지 실행하지 않는다.
- [x] **learner-home 로딩 스켈레톤 밀도(390)** — (2026-06-26 처리) '연습 대상' 로딩 스켈레톤을 빈 div 단일 셰이머에서 실제 카드 구조(마크 박스 + 이름/메타/요약 2줄 플레이스홀더)를 모사하도록 밀도 보강. `LearnerHome.tsx` (`.lh-skel__box`/`.lh-skel__line*`), reduced-motion 분기 동반 갱신. **검증: `npm run typecheck` PASS + `vite build` PASS.**
- [ ] **운영 티켓 자동 분류·처리 후속** — Claude Recipe headless 자동 수정 후보, 관리자 승인 후 이슈 등록·PR/작업
스레드 생성, 처리 결과 audit trail 확장은 아직 설계/승인 필요. 담당 그룹 자동 배정·우선순위 escalation·raw/rollup
보존기간 같은 운영 정책은 B3에서 이미 결정됨(수동 승인·미도입 고정). 자동 수정은 운영자 승인 전까지 실행하지 않는다.
검증 기준: 변경 후 `npm run typecheck`, `e2e/layout-visual-gate.spec.ts`(현재 12/12), 레이아웃 포커스 E2E(54), `e2e/session-layout.spec.ts`(8/8) 무회귀. 이전 후속 라운드는 빈상태 레이아웃(`typecheck`, `build`, layout gate 9/9, session-layout+session-review desktop 6/6), 사용자별 티켓 조회 UI(backend 29, settings 7, admin ticket 1), synthetic health sampler(backend 31, one-shot recorded_count 5), health retention/rollup(backend 36, dry-run OK), 티켓 중복 저장·수동 연결(backend 38, API types/typecheck/build OK, admin tickets E2E 2), M2 route-level seed recall 주입(py_compile, M2 focused 68)을 확인했다. 최신 다크 UI v2 라운드는 `learner.spec.ts`+`session-review.spec.ts` 9 passed, `session-layout.spec.ts` 8 passed, `layout-visual-gate.spec.ts` 9 passed로 별도 기록한다.
> B1 완료 항목(셸 구분선, 세션 종료 UX/다크테마, 아바타 PSD v2, SEO/공유 카드, 레이아웃 정렬, 권한 위임,
> 메일링, 아카이브 API, TTS voice map, 빈상태 레이아웃, 평가 실패 복구 UX, SSE 저장, live-coach 표면화,
> 음성 메타, 위기 게이트 UI, 운영 티켓/헬스 샘플러 등)의 상세·검증 로그는 아카이브 full-history 참조.
---
## B2. 환경 제약 — 이 워크스테이션에서 증거 생산 불가
- [ ] **`vnet.18ka.net` 공개 live 전환** — 코드/스크립트는 `vnet.18ka.net`·`api-vnet.18ka.net`을 허용하도록 갱신 완료. Tailnet 주소 `https://alpaca-home.taile93291.ts.net`은 dev-login까지 검증됐고, public OAuth redirect가 로컬/Tailnet 세션으로 붙지 않는 혼선을 막기 위해 Tailnet Google 버튼과 직접 Google 시작 URL은 `local_oauth_unavailable`로 차단한다. 공개 vnet은 현재 (1) `vnet.18ka.net`/`api-vnet.18ka.net` DNS 부재, (2) cloudflared 현재 인증 컨텍스트가 `18ka.net` zone이 아니라 `chanpaca.net` zone에 route dns를 생성, (3) Google Console redirect URI `https://api-vnet.18ka.net/auth/callback` 등록 필요 때문에 live 완료 아님. 현재 prod API 8001 자체는 `db:true`, `engine:true`로 건강하다. 상세: `docs/ops/tailscale-vnet-runtime-2026-06-27.md`.
- [ ] **실배포 `infra/.env` owner-secret fill-in + real preflight** — compose 템플릿과 더미 env smoke는 통과했지만 실제 배포지는 소유자 비밀값이 필요하다. 필수값: `APP_DB_PASSWORD`, `OAUTH_GOOGLE_CLIENT_ID`, `OAUTH_GOOGLE_CLIENT_SECRET`, `OPENAI_API_KEY`, `SESSION_SECRET`, production-safe engine/voice flags. 현재 로컬 stray 값(`ENGINE_MODE=claude_p`, prod sample TTS flag 등)은 운영값으로 복사하면 안 된다. 실제 secret 주입 후 `scripts/check-deploy-preflight.py` DB 포함 모드와 배포지 health를 별도 증거로 닫는다.
- [ ] **공개 Google OAuth 실제 `/turn` proof** — 로그인 가능한 계정으로 `storageState` 캡처 후 `E2E_PUBLIC_AUTH=1` + `chromium-public-auth` 1회 통과 필요. (소유자 지시로 이번엔 보류.)
- [ ] **음성 캐스케이드 live** — (2026-06-29 갱신) 로컬 API에서 실제 OpenAI STT/TTS 경로는 live WS smoke로 통과했다(P2 세션, MP3 발화 → transcript 52자 → reply 105자 → TTS binary 66 chunks/266,496 bytes). Session UI는 Web Audio unlock 재생과 AudioWorklet-first PCM16 capture로 보강했고, backend는 `format=pcm` raw chunk를 STT 업로드 직전 WAV로 정규화한다. `/voice/ws`는 provider-neutral `stt_result` control을 받아 EOT ready/pending을 route에서 판단한다. synthetic mic E2E는 AudioWorklet 경로로 통과했다. 남은 범위는 실제 Deepgram interim/final WSS, 물리 마이크, 공개 WSS, 50분 양방향 장시간 실측이다.
- [ ] **claude_cli ↔ Messages API 폴백 동일성**`ANTHROPIC_API_KEY`가 있어야 Messages API 경로를 돌려 동일성 검증 가능. claude_cli 경로는 게이트웨이 probe로 live 실측 완료.
- [x] **저항엔진 openness 곡선 DB 실증** — (2026-06-28 처리) `scripts/smoke-resistance-openness-db.py`로 실제 API 경로(dev-login→onboarding→P1 세션 2개→각 5턴)를 실행하고 DB `app.session_state`/`app.turns`/`app.turn_client_state`를 직접 조회했다. 공감 5턴은 API curve `0.0→0.0→0.095→0.133→0.171`, DB final `stage=탐색`, `effective_openness=0.17100003361701965`; 조언점프 5턴은 API curve 전부 `0.0`, DB final `stage=라포`, `effective_openness=0.0`, `resistance=0.9580000042915344`. 증거: `docs/ops/resistance-openness-db-smoke-2026-06-28.json`, `docs/ops/resistance-openness-db-smoke-2026-06-28.md`. **검증: `python -B -m py_compile scripts/smoke-resistance-openness-db.py`, `pytest app/test_state_machine_resistance.py app/test_evaluation_persistence.py -q` 9 passed, live smoke PASS.**
- [ ] **재부팅 후 watchdog smoke**`watch-public-runtime.ps1` + Scheduled Task가 재부팅 후 엔진/API/터널을 복구하고 public `/turn`이 통과하는지 실측. 재부팅 불가로 미실행(parser/ check-only 경로는 확인됨). 2026-06-27 점검에서 아직 DNS가 없는 `api-vnet.18ka.net`은 기본 watchdog 검사에서 제외했고, 개통 후 `-AdditionalPublicHealthUrls`로 명시 추가하도록 runbook을 보강했다.
- [x] **상주 엔진풀 RSS 실측** — (2026-06-28 처리) gateway 세션 1개를 열고 `claude -p` 자식 프로세스 RSS를 Windows WorkingSet 기준으로 계측했다. `health.sessions=1` 상태에서 gateway `python.exe` PID 51592는 after-turn RSS 16.2MB, 상주 `claude.exe` PID 51688은 RSS 363.3MB/Private 628.8MB였다. Claude CLI plugin runtime `bun.exe` 자식 2개는 합계 RSS 178.6MB였다. 세션은 측정 직후 `DELETE /session/{id}`로 종료했다. 증거: `docs/ops/engine-rss-smoke-2026-06-28.md`.
- [ ] **`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`.
- [ ] **실배포 `infra/.env` owner-secret fill-in + real preflight** — 필수 소유자 비밀값: `APP_DB_PASSWORD`,
`OAUTH_GOOGLE_CLIENT_ID`, `OAUTH_GOOGLE_CLIENT_SECRET`, `OPENAI_API_KEY`, `SESSION_SECRET`, production-safe
engine/voice flags. 로컬 stray 값(`ENGINE_MODE=claude_p`, prod sample TTS flag 등)은 운영값으로 복사 금지.
실제 secret 주입 후 `scripts/check-deploy-preflight.py` DB 포함 모드 + 배포지 health를 별도 증거로 닫는다.
- [ ] **공개 Google OAuth 실제 `/turn` proof** — 로그인 가능한 계정으로 `storageState` 캡처 후 `E2E_PUBLIC_AUTH=1`
+ `chromium-public-auth` 1회 통과 필요. (소유자 지시로 보류 중.) 실행 명령은 대시보드 "다음 실행 명령" 참조.
- [ ] **음성 캐스케이드 live** — 로컬 API 실제 OpenAI STT/TTS 경로는 live WS smoke 통과. 남은 범위: 실제 Deepgram
interim/final WSS, 물리 마이크, 공개 WSS, 50분 양방향 장시간 실측.
- [ ] **claude_cli ↔ Messages API 폴백 동일성**`ANTHROPIC_API_KEY`가 있어야 Messages API 경로 동일성 검증 가능.
claude_cli 경로는 게이트웨이 probe로 live 실측 완료.
- [ ] **재부팅 후 watchdog smoke**`watch-public-runtime.ps1` + Scheduled Task가 재부팅 후 엔진/API/터널을 복구하고
public `/turn`이 통과하는지 실측. 재부팅 불가로 미실행(parser/check-only 경로는 확인). DNS 개통 후
`api-vnet.18ka.net``-AdditionalPublicHealthUrls`로 명시 추가. 상세: `docs/ops/public-runtime-watchdog.md`.
---
## B3. 소유자(윤찬) 결정 필요 — 단독 결정 시 월권
## B3. 소유자(윤찬) 결정 — 전건 확정(2026-06-30)
> 결정되면 대시보드 `결정 필요` 탭과 관련 Phase 항목을 갱신한다.
- [x] **운영 티켓·가용성 정책 결정** — 결정됨(2026-06-30): 헬스 raw 90일·rollup 365일 보존, 우선순위 escalation은 운영자 수동 승인, SLA 미표기(관측 샘플 이력 유지), 담당 그룹 자동 배정 미도입. 후속: retention 기본값(raw 90/rollup 365) 적용·검증. escalation/그룹 자동화는 미도입으로 고정.
- [x] **평가설계 (H1: 실험/통제군·3척도·κ/ICC)** — 결정됨(2026-06-30): 평가자 신뢰도 κ≥0.70 & ICC≥0.75, 환각률 ≤3%, 독립표본 t-검정(α=0.05), 20명 실험/통제군 무작위 배정, 단회기 50분·3척도 pre-post 유지. 후속: KPI 측정계획·report threshold에 κ/ICC/환각률 반영. 실제 20명 파일럿 evidence·통계해석·공식 효과성 판정은 별도 GATE.
- [x] **s2s 2차 PoC 채택 여부** — 결정됨(2026-06-30): s2s 2차 PoC 유지. 캐스케이드 1차를 운영 기준으로 두고 `docs/decisions/voice-s2s-poc.md` 비교 PoC를 계속 연다. 최종 채택/폐기는 비교 리포트 이후. 후속: s2s 비교 PoC 구현·레이턴시/안전/감사 실측.
- [x] **서비스용 페르소나 음성 모델·권리 정책** — 결정됨(2026-06-30): 상업 라이선스가 명확한 대체 TTS provider 채택. Higgs v3 무참조 synthetic 샘플과 `VIGNETTE_VOICE_POC_SAMPLE_TTS=true` dev sample provider는 개발 플래그로만 유지하고, 실존 인물/성우 reference 사용 금지를 유지한다. 후속: 대체 TTS provider 선정·통합, dev/서비스 경로 분리 유지.
- [x] **fast-loop 외부 API 허용** — 결정됨(2026-06-30): 외부 폴백 허용. 로컬 상주 분류기 기본 + 외부 Haiku·Solar 폴백, 외부 전송 시 데이터주권·로그 보존·동의 가드 적용. 후속: 폴백 경로·전송 가드·관측 구현, 로컬 우선/외부 폴백 라우팅 정책 명시.
- [x] **재귀학습 fine-tuning 범위** — 결정됨(2026-06-30): fine-tuning 포함. few-shot 자동갱신에 더해 fine-tuning까지 범위에 두되, 데이터셋은 동의 범위·추가 축어록 수급 결정이 닫힌 뒤 구성한다. few-shot 자동갱신은 선행 가능.
- [x] **자유연습 기본값** — 결정됨: 기본 ON(+피드백 권장 배지+교수자 토글). 미동의·미성년 개인정보 gate만 차단한다. 근거: `docs/redteam/MASTERPLAN_REVISIONS.md` F-10 승인.
- [x] **추가 축어록 수급 규모·일정** — 결정됨(2026-06-30): 20건을 1개월 내 수급. 후속: 실제 수급·동의·정제는 임상/한신대 거버넌스 게이트와 연계.
- [x] **백엔드 언어 방향** — (2026-06-28 결정, 2026-06-29 보강) Node.js를 장기 교체 대상과 신규 분리 서비스 우선 스택으로 삼되, 현재 FastAPI 전면 재작성은 납품 일정 리스크라 보류한다. 전환은 계약 우선 strangler 방식으로 진행한다. 고정된 내부 경계는 `app/contracts/engine_gateway.py`의 gateway request/response/SSE 계약과 `gateway-default` sentinel, `structured_payload_from_response()`, `scripts/check-engine-gateway-contract.mjs` schema-driven Node conformance runner, `GatewayPromptParts`, `app/session_read_model.py`, `app/persona_read_model.py`, `app/persona_generation_contract.py`, `SessionEvaluationWrite.from_result()/from_error()`다. FastAPI route는 auth/RLS DB read/persistence, teacher/admin gate, RAG/LLM side effect, session lifecycle, provenance/error mapping을 계속 소유한다. 2026-06-29 후속으로 Python gateway의 `provider/model` 응답 메타 규칙을 helper로 모으고, Node runner가 `gateway-default` request sentinel과 resolved response model 분리를 검증하며, current-turn prompt split과 missing-user 400 경계를 테스트로 고정했다. 결정 기록: `docs/decisions/backend-node-transition.md`. **검증: `node scripts/check-engine-gateway-contract.mjs --json`, gateway contract 27 passed, persona contract+review 39 passed, backend focused 120 passed, `npm run check:api-types`, `npm run typecheck`.**
- [x] **Node 전환 경계 보강 — 페르소나 생성 계약 + 평가 저장 packet** — (2026-06-29 처리) `app/persona_generation_contract.py`가 페르소나 draft structured schema, prompt bundle id/version/hash, `GenerateResponse` payload extraction, generated draft coercion/default fallback을 소유한다. `SessionEvaluationWrite.from_result()/from_error()``app.session_evaluation` 저장 packet(`status/source/scope/stage/payload/error`) 생성을 소유한다. Node conformance runner는 stream event 목록과 payload 검증을 `engine_gateway_schema.v1.json``StreamPacket.oneOf`, payload `$ref`, `x-engine-gateway-sse-events`에서 읽는다. `routes/personas.py`는 auth, source 등록·조회, RAG evidence, engine 호출, provenance 조립, HTTP error mapping을 유지하고, `routes/sessions.py`/`routes/eval.py`는 factory로 named packet을 만들어 저장한다. **검증: `py -3.11 -X utf8 -B -m py_compile app\persona_generation_contract.py app\routes\personas.py app\persona_read_model.py app\test_persona_generation_contract.py app\test_persona_review.py engine_gateway\test_gateway_model.py app\session_persistence.py app\routes\sessions.py app\routes\eval.py app\services\evaluator.py app\services\live_coach.py`, `py -3.11 -X utf8 -B -m pytest -p no:cacheprovider app\test_persona_generation_contract.py app\test_persona_review.py -q` 39 passed, backend focused 120 passed, `node scripts\check-engine-gateway-contract.mjs --json`, `npm run check:api-types`, `npm run typecheck`.**
> 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로 남는다.
---
## B4. 외부 거버넌스 — 한신대/데이터 steward 서면 증거
- [ ] **한신대 데이터/SSO 거버넌스 게이트** — SSO claim mapping, 추가 축어록 수급, 미성년 원본 활용동의, 개인정보 처리방침. 로컬 문서 골격 `docs/ops/hanshin-data-governance-gate.md` 준비됨, 외부 서면 증거 필요. (소유자 지시로 이번엔 보류.)
- [ ] **L1 스택 정합성·저작권 등재 문서 반영** — 백엔드 방향은 Node.js 교체 가능성 우선으로 결정됐지만, 신청서/저작권 등재 문서에는 현재 FastAPI 유지 사유와 계약 우선 전환 계획을 반영해야 한다. 로컬 결정 기록은 `docs/decisions/backend-node-transition.md`; 외부 제출/등재 증거가 필요하다.
- [ ] **한신대 데이터/SSO 거버넌스 게이트** — SSO claim mapping, 추가 축어록 수급, 미성년 원본 활용동의, 개인정보
처리방침. 로컬 골격 `docs/ops/hanshin-data-governance-gate.md` 준비됨, 외부 서면 증거 필요. (소유자 보류 중.)
- [ ] **L1 스택 정합성·저작권 등재 문서 반영** — 백엔드는 Node.js 교체 가능성 우선으로 결정됐지만, 신청서/저작권
등재 문서에는 현재 FastAPI 유지 사유와 계약 우선 전환 계획을 반영해야 한다. 로컬 결정 기록은
`docs/decisions/backend-node-transition.md`; 외부 제출/등재 증거 필요.
---
## Phase 3 파일럿 게이트 (실참여자 필요)
- [ ] 20명 교육용 파일럿 운영 / 효과성·KPI 측정(SUS·자기효능감·κ/ICC·환각률) / 재귀학습 데이터셋 approved 산출 / 개인정보·동의 감사. 문서/checker와 dry-run exporter는 준비됨(`docs/phase3/*`, `scripts/check-phase3-artifacts.py`, `scripts/export-recursive-dataset.py`). 2026-06-27 checker 강화로 CSV enum, KPI metric required fields, approved export의 PII pass·κ/ICC·withdrawn exclusion·consent scope·file sha256 false-positive 차단을 추가했다. 2026-06-28에는 3척도 pre/post aggregate evidence 원장 `app.learner_prepost_measure`, 학습자 본인용 `GET/PUT /users/me/prepost-measures`, `SessionReview` 입력 UI를 추가했고, `app.services.phase3_kpi_export`/`scripts/export-phase3-kpi.py``prepost_measures.csv``kpi_report.json` scaffold를 생성한다. participant id는 가명화하고 3척도 paired normalized delta, complete/missing pair를 계산한다. `phase3_kpi_contract.py`는 KPI metric 이름·필수키·`computed_prepost`/`design_pending` status 값을 공유해 exporter/checker/test drift를 줄이고, checker는 status unknown 값을 error로 처리한다. 공식 문항 확정, 통계 검정, 실험/통제군 배정, 실제 approved evidence는 파일럿·steward/legal 검수·IAA 통과 시점. **검증: `py -3.11 -X utf8 -m py_compile apps\api\app\services\phase3_kpi_contract.py apps\api\app\services\phase3_kpi_export.py apps\api\app\test_phase3_kpi_export.py apps\api\app\test_phase3_artifact_checker.py scripts\check-phase3-artifacts.py scripts\export-phase3-kpi.py`, `py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_phase3_kpi_export.py app/test_phase3_artifact_checker.py -q` 10 passed, `py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_user_support_tickets.py app/test_runtime_policy.py app/test_admin_ops.py app/test_phase3_kpi_export.py app/test_phase3_artifact_checker.py -q` 46 passed, `py -3.11 -X utf8 scripts\export-phase3-kpi.py --help`, `py -3.11 -X utf8 scripts\check-phase3-artifacts.py --help`.**
---
## 이번 세션에 닫은 것(참고)
- 레이아웃 시각 수용: 기본 게이트 7/7 + 적대적 재검수 7/7 accept에 더해 빈 회기리뷰, 교수자 리뷰, 학생 분석 목록/상세 gate를 추가한 현재 12/12 — DONE.
- live 운영 증거: engine config 운영값, 상주 엔진풀 probe(TTFT/cost/세션재사용), 상주 엔진풀 RSS 실측, Postgres RLS/audit smoke 5 checks PASS, turn cost telemetry(app.turns 13행) — DONE.
- P1 서연 음성 아트 PoC: Higgs v3 무참조 synthetic seed + 5개 정서/속도 변주(mp3/wav) 생성. 실존 reference voice 미사용. `/voice/ws`에는 dev-only sample TTS provider로 연결했고 live text_turn smoke에서 `p1-sample-poc` binary 20 chunks/78,573 bytes 수신 확인. 이 provider는 TTS override만 담당한다(마이크/STT는 OpenAI 키 필요). 프로덕션 탑재는 라이선스/권리 결정 전 금지.
- Session.tsx 음성 guard/view-model P2 리팩터 + capture/EOT 계약 보강: `isVoiceStatusBusy()``sessionVoiceStatusView()`가 마이크 busy/disabled, aria label, transcript/response/status label, 텍스트 입력 차단 계산을 소유한다. Session mic capture는 `AudioWorklet`-first PCM16 + `MediaRecorder` fallback이고, `/voice/ws``stt_result` control로 EOT ready/pending을 route에서 판단한다. 검증은 web typecheck/build, `session-mvp`, `voice-success` synthetic UI, `session-layout`, `test_voice_ws.py/test_voice_service.py` 범위만 인정한다. 실제 Deepgram WSS, 물리 마이크·공개 WSS·50분 장시간 실측은 B2 음성 캐스케이드 live 항목에 계속 남긴다.
- 부수: RLS smoke 스크립트 cleanup RLS 컨텍스트 버그 수정, `vignette_app`(NOBYPASSRLS) dev 역할 프로비저닝.
- 평가 SSOP 2차: `TurnRecord.evaluation``app.feedback_scores`/`turn_technique`/`turn_client_state`/`supervisor_comment`/`alternative_utterance`로 정규화 적재·복원하고, submit/stream/voice 저장 경로를 `app.turn_runtime`으로 공통화했다. 원시 평가 테이블은 evaluator/admin RLS로 제한하고 서버가 evaluator 컨텍스트로 적재한다. `app.turns` insert는 `RETURNING id` 확인으로 silent success 제거. **검증: `pytest app/test_evaluation_persistence.py -q` 6 passed, 관련 회귀 45 passed, `pytest app/` 119 passed, `pytest engine_gateway/` 7 passed.**
- 라이브 코칭 1차 + 이력 UI/RAG sync + 학습자 리뷰 3열 workbench: `app/services/live_coach.py`, `data/kb/live_coaching_workbook_0615.json`, `data/kb/live_coaching_sources/*.json`, `POST /sessions/{id}/live-coach`, `GET /sessions/{id}/live-coach`, `POST /kb/live-coach/source-packs/sync`, `app.live_coach_events`를 추가해 0615 사례개념화 워크북·DSM·공식 지침 요약 기반 코칭을 구조화했다. 세션 UI는 코칭 모드에서 AI 코치 아바타 말풍선, 근거 모달, 학습자 발화 우측 `C` 마커, 채팅 위 스크롤 이력 오버레이를 제공한다. 저장 payload는 PII 마스킹 excerpt와 코칭 구조화 JSON으로 제한한다. 공식 자료는 허가된 source pack의 version/citation/summary로만 코칭에 넣고 장문 원문·공식 문항 재현은 하지 않는다. 같은 source pack은 `app.services.source_pack_sync`로 evaluator 전용 RAG에 증분 색인하며, active `content_hash`가 바뀌면 `kb.document.version`을 최신+1로 올려 UNIQUE 충돌을 피한다. 2026-06-28 refactor-governance P4에서는 live turn의 프로세스 로컬 source pack snapshot은 유지하되 관리자 sync/CLI가 `refresh=True`로 캐시를 비우고 repo 파일을 다시 읽게 해 stale `content_hash` 비교를 막았다. 학습자 `SessionReview` 데스크톱은 요약/흐름, 축어록, 평가 rail의 3열 workbench와 하단 워크시트로 재배치했고, empty review는 2열 이하로 유지한다. 2026-06-29에는 워크시트/pre-post/교수자 메모 입력에 name/autocomplete/aria-label과 공유 focus token을 보강하고, 발화 이동 버튼 aria-label을 추가했다. **검증: `python -m compileall app -q`, 과거 `pytest app/test_live_coach_sources.py app/test_orchestrator_masking.py app/test_session_turn_persistence.py -q` 27 passed, source sync `py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_live_coach_sources.py -q` 9 passed, H2 related `py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_live_coach_sources.py app/test_orchestrator_masking.py app/test_session_turn_persistence.py -q` 44 passed, `py -3.11 -X utf8 scripts\sync-persona-sources.py --help`, `npm run generate:api-types`, `npm run check:api-types`, `npm run typecheck`, `npm run build`, `npm run e2e -- e2e/layout-visual-gate.spec.ts e2e/session-layout.spec.ts` 15 passed. 3열 workbench 검증: `npm run typecheck`, `npm run build`, `npx playwright test e2e/session-review.spec.ts --project=chromium-desktop --workers=1` 3 passed, `npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run --workers=1` 9 passed, `04-session-review-desktop.png` 직접 확인. 로컬 DB smoke: sync 4 sources/15 chunks embedded, 재호출 skipped_unchanged 4, `official_suicide_risk_guidelines` source_id 필터 검색 성공.**
- 계약 SSOT 7차: FastAPI OpenAPI를 `apps/web/src/lib/api.gen.ts`로 생성하는 `npm run generate:api-types`와 stale 체크용 `npm run check:api-types`를 유지한다. `TurnResponse`/`SessionEndResponse`에 이어 `MeResponse`, `AuthConfigResponse`, `ConsentResponse`, `PersonaSummary`, `PersonaReviewSummary`, admin/user/engine DTO, review leaf DTO, `CrisisResourceResponse`, teacher safety/growth leaf DTO, `LearnerSessionsResponse`, `LearnerDashboardResponse`, `SessionReviewResponse`, worksheet aggregate, `TeacherDashboardResponse`, `TeacherSessionSummary`, `TeacherLearnerGrowth`, `SessionStartResponse`, `SessionDetailResponse`, `SessionDetailTurn`를 생성 타입 alias로 전환했다. 세션 응답 stage는 `StageLabel` enum으로 OpenAPI에 고정했고, 69차에서는 `ReviewPhaseSegment.key/label`, `SessionReviewResponse.reachedPhase`, `EvaluationSummary.stage`, 평가 트리거 응답 stage, `TeacherSessionSummary.stage`, `TeacherGrowthPoint.stage`도 union/null 계약으로 축소했다. 71차에서는 공개 공유 응답 `PublicSessionShareResponse.reachedPhase`와 live-coach 이력 `LiveCoachEvent.stage``StageLabel | null` 계약으로 축소했다. 72차에서는 `app/stage_contract.py``StageLabel`, `ReviewPhaseKey`, legacy stage code 정규화, unknown→null 판단, 리뷰 phase key 매핑을 소유하게 해 `turn_runtime.py`, `session_read_model.py`, `routes/eval.py`의 중복 stage 규칙을 제거했다. generated optional/default 차이는 UI form state, notification default helper, 배열 렌더링 fallback, live-coach `단계 미상` 표시로 흡수한다. `.github/workflows/api-contract.yml`은 PR/master push에서 `npm run check:api-types` drift를 막는다. **검증: `py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_evaluation_persistence.py app/test_evaluator_model_routing.py app/test_teacher_dashboard.py app/test_rbac_idor.py app/test_session_turn_persistence.py -q` 50 passed, `py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_session_share.py app/test_live_coach_sources.py app/test_session_turn_persistence.py app/test_orchestrator_masking.py -q` 43 passed, `py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_stage_contract.py app/test_session_share.py app/test_session_turn_persistence.py app/test_evaluation_persistence.py app/test_teacher_dashboard.py -q` 42 passed, `py -3.11 -X utf8 -m py_compile apps/api/app/stage_contract.py apps/api/app/turn_runtime.py apps/api/app/session_read_model.py apps/api/app/routes/eval.py apps/api/app/routes/sessions.py apps/api/app/routes/share.py apps/api/app/services/live_coach.py`, `npm run check:api-types`, `npm run typecheck`, `npm run build`, 과거 `learner.spec.ts + session-review.spec.ts + teacher.spec.ts + layout-visual-gate.spec.ts` 32 passed.**
- 위기게이트 109 1차: 실제 자해·자살 신호는 LLM/엔진 호출 전 중단하고 `crisis_resource(109)``conversation_stopped`를 REST/SSE/voice 응답에 싣는다. `app.safety_events` detail 적재와 교수자 대시보드 안전 알림 큐까지 연결했다. 안전 이벤트 DB insert payload도 회귀화했다. 임상 문안, 실시간 push/메일, 지역 자원 확장은 후속. **검증: `pytest app/test_session_turn_persistence.py app/test_voice_ws.py -q` 20 passed, `pytest app/test_teacher_dashboard.py app/test_session_turn_persistence.py app/test_voice_ws.py -q` 21 passed.**
- 교수자 성장 추적/리뷰 상세 진입 1차: `teacher_dashboard`가 정규화된 턴 평가를 기반으로 학습자별 적절성·라포·기술 사용 추이를 집계하고, 교수 콘솔에 최근 회기/항목별 성장 카드와 안전 알림 큐를 함께 표시한다. 종료 회기 리뷰 대기 행과 최근 회기 행은 `/teach/session/:sessionId/review`로 이어지고, 교수자는 회기 리뷰와 사례개념화 워크시트를 읽기 전용으로 검토한다. 학습자 워크시트 저장은 계속 learner 전용이다. **검증: `pytest app/test_teacher_dashboard.py app/test_session_turn_persistence.py app/test_voice_ws.py -q` 21 passed, `pytest app/test_rbac_idor.py app/test_teacher_dashboard.py -q` 9 passed, `npm run check:api-types`, `npm run typecheck`, `npm run build`, `npx playwright test e2e/teacher.spec.ts --project=chromium-single-run --workers=1` 4 passed.**
- 교수자 검토 상태 1차: `app.session_review_status`에 교수자 회기 검토 상태를 저장하고, teacher dashboard 응답에 `review_status/review_note/reviewed_at`를 포함한다. `PUT /teacher/sessions/{session_id}/review-status`로 교수자 메모 저장과 검토 완료 처리를 수행하며, 완료된 회기는 pending queue에서 제외한다. 교수자/관리자의 `/sessions/{id}/review` 읽기 허용은 유지하고, 학습자 워크시트 저장은 learner 전용으로 계속 제한한다. 교수 콘솔 검토 큐는 숨김 처리하지 않고 상단 triage로 노출하며, 학습자 리뷰 화면에는 교수자 전용 grid 빈칸을 예약하지 않는다. **검증: `pytest app/test_teacher_dashboard.py app/test_rbac_idor.py app/test_learner_dashboard.py -q` 12 passed, `npm run check:api-types`, `npm run typecheck`, `npm run build`, `teacher.spec.ts` 4 passed, `session-review.spec.ts` 2 passed, `layout-visual-gate.spec.ts` 7 passed.**
- 학습자 개인화 대시보드 1차: 교수자 성장 집계와 중복되던 점수/라포/기법 계산을 `app.services.session_metrics`로 공용화하고, `GET /sessions/dashboard`가 본인 세션 기반 `overview/growth/persona_progress/achievements/recent_feedback`를 반환한다. 학습자 홈은 누적 회기, 리뷰 대기, 최근 평가, 라포 흐름, 페르소나별 진행, 최근 피드백, 마지막 세션 리캡을 실제 서버 데이터로 표시한다. 성취는 공식 등급/수료가 아니라 실제 연습 milestone만 사용한다. 2026-06-29 refactor-governance 패스에서 `session_metrics.turn_technique_label()`이 DB 재수화 계약의 `label_ko`를 우선하고 기존 `label/name/id/code` fallback을 유지하도록 고정했다. **검증: `py -3.11 -X utf8 -B -m pytest -p no:cacheprovider app/test_evaluation_persistence.py app/test_evaluator_model_routing.py app/test_session_turn_persistence.py app/test_rbac_idor.py app/test_teacher_dashboard.py app/test_learner_dashboard.py -q` 59 passed, 기존 `npm run check:api-types`, `npm run typecheck`, `npm run build`, `learner.spec.ts` 6 passed, `session-review.spec.ts` 2 passed, `teacher.spec.ts` 4 passed, `layout-visual-gate.spec.ts` 7 passed, `session-layout.spec.ts` 4 passed.**
- 페르소나 스튜디오/RAG 저작 10차: 교수 콘솔의 JSON 패널을 분리해 teacher/admin 전용 `/teach/personas` 3열 스튜디오(좌: 저작 흐름·카탈로그·검수 큐, 중앙: RAG 자료 등록·항목형 편집 탭, 우: 검증·RAG 근거·검수 결정)로 옮겼다. 2026-06-27 UI 정리에서 중복 네이티브 파일 입력을 커스텀 SSOT 첨부 드롭존으로 교체하고, 저작 스테퍼·탭별 작성 가이드·검증 dot 정렬을 보강했다. 2026-06-28 항목형 목록 UI 정리에서 자동사고, 회기 시나리오, 말투 filler/verbal tic/nonverbal cue, 역린·금기 응답·금기어를 행 추가/삭제 UI로 전환했고, 저장 payload는 기존 배열 schema를 유지하며 빈 항목만 저장 직전에 제거한다. 프롬프트 탭은 raw JSON textarea 대신 L1 카드·인적 범주·임상 배경·말투·수치 파라미터·역린·회기 시나리오·추가 계약 섹션으로 같은 draft 데이터를 라벨형 검토 UI에 표시한다. 레이아웃 근거와 생성 시안은 `docs/ops/layout-research-2026-06-27/persona-dashboard-layout-guideline.md`에 묶었다. `POST /personas/sources`는 첨부/붙여넣기 자료를 PII 마스킹 후 raw 원문 hash-only 증거를 `kb.raw_source_artifact`에 따로 기록하고, sanitized 파생본만 `kb.source/document/chunk`에 evaluator 전용(`visible_to=['evaluator']`, `sensitivity=2`) 근거 문서로 등록한다. `rag.index_document()``sensitivity=3` 또는 raw marker chunk를 DB 접근 전에 차단해 raw 원문이 `kb.chunk`/embedding/FTS에 들어가지 않게 한다. `POST /personas/drafts/generate``source_id` 기반 RAG 검색 결과만 생성 프롬프트에 넘긴다. 생성 응답과 draft `source_provenance`에는 source id, doc/content hash, chunk id, prompt bundle id/version/hash(`persona-draft-rag@2026-06-28.1`)를 남긴다. P1~P7 시스템 페르소나는 DB 저작 카탈로그의 초기 부트스트랩으로 승격했고, `materialize_seed_personas()`는 누락분만 insert해서 교수 편집본이나 `archived` 보관본을 덮어쓰지 않는다. `scripts/materialize-persona-seeds.py`는 같은 seed/version manifest를 dry-run 기본으로 보고하며, `--apply`일 때만 DB pool을 초기화한 뒤 기존 idempotent materializer를 호출한다. `scripts/sync-persona-sources.py``app.services.source_pack_sync`는 repo-managed source pack의 active `content_hash`를 DB에서 비교하고, 변경 시 `kb.document.version`을 최신+1로 색인한다. 교수자는 공개 목록에서 승인본을 다음 버전 draft로 복제해 수정하고, 불필요한 페르소나는 기존 회기 FK 보존을 위해 같은 code family 전체를 `archived`로 보관 처리한다. `app/persona_read_model.py`는 catalog/review/draft/source/evidence DTO와 mapper를 route에서 분리해 schema 이름과 `source/degraded` fail-closed 경계를 유지한다. 암호화 blob/vault 기반 원문 실저장, 임상팀 최종 검수/서면 evidence, 루브릭·이론 콘텐츠 외부화는 후속. **검증: `python -B -m py_compile app/services/rag.py app/routes/personas.py app/routes/kb.py app/test_persona_review.py app/test_live_coach_sources.py`, `python -B -m pytest -p no:cacheprovider app/test_persona_review.py app/test_live_coach_sources.py -q` 38 passed, `python -B -m py_compile app/persona_read_model.py app/routes/personas.py`, `python -B -m pytest -p no:cacheprovider app/test_persona_review.py -q` 33 passed, `python -B -m pytest -p no:cacheprovider app/test_persona_review.py app/test_session_turn_persistence.py -q` 55 passed, `npm run check:api-types`, `npm run typecheck`, `npm run build`, `py -3.11 scripts\materialize-persona-seeds.py --json` dry-run `manifest_count=7`, `py -3.11 scripts\sync-persona-sources.py --help`, `npx playwright test e2e/teacher.spec.ts --project=chromium-single-run --workers=1` 8 passed, 프롬프트 탭 포함 `layout-visual-gate.spec.ts` 9 passed, `session-layout.spec.ts` desktop/mobile 8 passed.**
- 페르소나 생성 계약 11차 + 평가 저장 packet: `app/persona_generation_contract.py`가 draft structured schema, prompt bundle, legacy/structured `GenerateResponse` payload extraction, generated draft defaults/coercion을 소유한다. 기존 `routes/personas.py`는 teacher/admin gate, RAG evidence 수집, engine request, provenance 조립, DB/HTTP error mapping을 계속 담당한다. `SessionEvaluationWrite.from_result()/from_error()`는 session evaluation 저장 packet 생성을 소유해 route의 long keyword bag을 제거했다. Node conformance runner는 `StreamPacket.oneOf`, payload `$ref`, `gateway-default` request sentinel과 resolved response model 분리를 읽어 event/payload/default-routing 검증 중복을 줄였고, `GatewayPromptParts`는 current-turn prompt split 경계를 명명했다. **검증: persona contract+review 39 passed, backend focused 120 passed, Node conformance OK, gateway contract 27 passed, `npm run check:api-types`, `npm run typecheck`.**
- M1 비언어 이벤트 4차: voice learner turn에 이미 저장하던 `audio_ref`/`silence_ms`/`speech_rate`/`barge_in`을 리뷰 API `nonverbal` 이벤트로 노출하고, 회기 리뷰 축어록에 침묵·발화 속도·끼어듦·음성 입력 칩을 표시했다. 2차에서는 `app.turns.provider_events JSONB``TurnRecord.provider_events`를 추가해 WebSocket control/STT provider 이벤트를 allowlist·size limit 후 보존했고, 3차에서는 저장 전 sanitizer에서 내부 taxonomy `event_type`/`category`를 붙인다. 4차에서는 인증된 회기 리뷰에 한숨·울음·웃음·호흡·운율·배경소음 계열만 한글 label/detail 칩으로 파생 노출한다. 이번 보강에서는 `/health`·runtime readiness·deploy preflight가 음성 메타 5개 컬럼 전체를 확인하고, 실제 Session 마이크 UI가 `audio_end`에 browser voice activity/trailing silence 메타를 보내며, DB-backed voice WS/UI E2E가 `/review` `nonverbal` 칩까지 이어지는지 확인한다. raw transcript/text/provider/source/raw type은 응답에서 제외하고, 공개 공유 카드에는 축어록과 provider raw를 싣지 않는다. 실제 provider 기반 한숨·울음 감지, 역량 지표화 정책, live 마이크/WSS 장시간 실측은 후속. **검증: `python -X utf8 -m pytest -p no:cacheprovider app/test_runtime_policy.py app/test_session_turn_persistence.py app/test_voice_ws.py app/test_voice_service.py -q` 77 passed, `PLAYWRIGHT_PORT=5197 npx playwright test e2e/voice-success.spec.ts --project=chromium-single-run --workers=1` 2 passed, `PLAYWRIGHT_PORT=5199 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1` 5 passed, `npm run typecheck`, preflight skip-db PASS.**
- M2 다회기 케이스 아크 11차: DB 세션 생성 시 `(persona_id, learner_id)` 기준 `case_profile`을 upsert하고, `session_no`를 트랜잭션 안에서 원자 증가시키며, `InProcSession.case_id`가 매회 새 `runtime_case_id`가 아니라 안정 `case_id`를 가리키게 수정했다. 시작/submit/stream/voice 경로는 case recall cache를 사용한다. 세션 종료 시 마스킹 축어록 기반 fallback `session_summary.digest``case_profile.case_digest`, `rapport_trajectory`, `alliance_level`을 갱신하고, 다음 회기 seed recall에서 `case_digest`·직전 `session_summary`·client-visible non-contradicted `pinned_fact`를 함께 조립한다. 3차에서는 마스킹된 client-visible 발화에서 `[NAME]`/`[ORG]` identity와 명시적 상담 약속만 보수적으로 `pinned_fact`에 upsert했고, 4차에서는 삽입 또는 값 변경 시 `pinned_fact_history`에 append-only 이력을 남긴다. 5차에서는 명시적 상담 약속 철회/부정만 기존 non-locked `agreement:counseling` fact를 `contradicted`로 격리하고 history reason `contradiction`을 남긴다. 6차에서는 세션 종료 저장 성공 뒤 마스킹된 client-visible 내담자 발화만 `app.turn_embedding`에 BGE-M3 dense/sparse로 `ON CONFLICT (turn_id) DO NOTHING` 색인한다. 7차에서는 submit/stream 공통 `_prepare_turn_context()` 경계를 만들고 DB seed recall·pinned fact가 다음 턴 EngineMessage L2/L4에 raw 이름 마스킹 상태로 주입되는 route-level 회귀를 추가했다. 8차에서는 `SessionDigestInput`/`SessionDigestResult`/`SessionSummaryWrite`로 종료 digest 입력·fallback 결과·DB write 인자 경계를 명시했다. 9차에서는 `DigestQualityAssessment`/`SessionDigestWorkerOutcome`로 LLM digest 후보 local quality harness를 추가해 빈/짧은 digest, raw forbidden substring, 내부 평가·CCD·상태 marker, 잘못된 `S{session_no}:` prefix를 fallback 유지 대상으로 판정한다. 10차에서는 `session_digest_worker.py``CompressionJob``GenerateRequest` 변환, 주입형 engine/audit 호출, accepted-only `session_summary.digest/compressed_by/token_count``case_profile.case_digest` 적용 계획, one-shot DB loader를 소유한다. 11차에서는 `scripts/run-session-digest-worker.py` dry-run/apply runner와 `SESSION_DIGEST_WORKER_ENABLED=false` 기본값의 세션 종료 background scheduler 골격을 추가하고, `compressed_by IS NULL` loader/apply CAS로 이미 압축된 세션 재실행 race를 막는다. LLM digest worker 입력 후보는 client-visible `text_masked` turn과 open thread로 제한하고, 종료 상태 수치·평가 payload·CCD·raw text는 압축 prompt에 넣지 않는다. `/sessions/{id}/end``digest_pending`은 CompressionJob 생성 여부를 알리는 비동기 압축 필요 신호로 유지한다. 새 contradicted fact는 임의 생성하지 않고, 같은 값 재확인은 history를 늘리지 않으며, `locked` fact는 건드리지 않는다. learner-owned `case_profile` 기준 RLS insert/update를 추가했고, 관계갈등·위기·임상 추론은 자동 pinning/모순 처리에서 제외했다. 관계·임상 fact 승격 기준, 실 provider 장시간 운영, 임상 골든셋 품질평가, 재압축은 후속. **검증: `py -3.11 -X utf8 -B -m pytest -p no:cacheprovider app/test_session_digest_worker.py app/test_session_memory.py -q` 30 passed, `py -3.11 -X utf8 -B -m pytest -p no:cacheprovider app/test_session_digest_worker.py app/test_session_memory.py app/test_session_turn_persistence.py app/test_orchestrator_masking.py app/test_runtime_policy.py -q` 87 passed, `py -3.11 -X utf8 -B -m py_compile app/services/session_digest_worker.py app/services/memory.py app/test_session_digest_worker.py app/test_session_memory.py`, 기존 `py -3.11 -X utf8 -B -m pytest -p no:cacheprovider app/test_session_memory.py -q` 20 passed, 과거 `py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_session_memory.py app/test_session_turn_persistence.py app/test_orchestrator_masking.py app/test_runtime_policy.py -q` 74 passed, `npm run check:api-types`, `npm run typecheck`.**
- C1 사례개념화 워크시트 4차: `SessionReviewResponse.caseWorksheet`와 리뷰 화면 카드가 탐색 11항목·호소 5영역·인지삼제/감정·보호/방해·생물심리사회 목표 초안을 축어록 근거 기반으로 제공한다. 학습자가 편집한 저장본은 `PUT /sessions/{id}/review/worksheet``app.case_worksheet`에 저장되고, 이후 `GET /review``saved_by_learner` 저장본을 자동 초안보다 우선 반환한다. 2026-06-28에는 `data/rubrics/case-worksheet-rubric.json`, schema, loader/validator service, `scripts/check-case-worksheet-rubric.py`를 추가해 임상팀 확정 루브릭을 외부 JSON으로 받을 scaffold를 만들었다. 현재는 `status=scaffold_only`, `scoring_enabled=false`이며 5개 section/28개 item key와 sha256 provenance만 검증한다. 64차 refactor-governance 패스에서 워크시트 템플릿 key source를 `CASE_WORKSHEET_SECTION_SPECS`/`case_worksheet_template_item_keys()`로 명시해 CLI/test가 더미 턴 없이 같은 생성 spec을 검증하게 했다. 73차에서는 기존 `app.session_review_status``worksheet_status`/`worksheet_note`/`worksheet_reviewed_at`을 추가하고, `PUT /teacher/sessions/{id}/review-status`와 교수자 `SessionReview` 카드에서 승인·수정요청·반려 수동 판정을 저장/표시한다. 임상팀 확정 루브릭 콘텐츠, AI 추출/채점 적용·calibration, 승인 후 잠금·재제출 정책, 재귀학습 데이터셋 approved 연계는 후속 gate다. **검증: `py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_session_turn_persistence.py app/test_teacher_dashboard.py app/test_rbac_idor.py app/test_case_worksheet_rubric.py -q` 40 passed, `py -3.11 -X utf8 scripts/check-case-worksheet-rubric.py --json` PASS, `py -3.11 -X utf8 -m py_compile` focused files, `npm run generate:api-types`, `npm run check:api-types`, `npm run typecheck`, `npm run build`, `npx playwright test e2e/session-review.spec.ts --project=chromium-desktop --workers=1` 3 passed, 로컬 API smoke 세션 생성→저장→`GET /review` `saved_by_learner:local smoke saved worksheet`.**
- C3 이론모드 2차: `theory_mode``TurnContext`/sessions/voice/evaluator뿐 아니라 `build_turn_messages`의 엔진 메시지까지 전달된다. 프론트는 `persona.theory_target` 기준 기본값을 유지하되, 세션 시작 전 `humanistic`/`cbt`/`integrative` segmented control로 학습자가 명시 선택하고 `POST /sessions``theory_mode`로 보낸다. 2026-07-01에는 교수자 수동 session/turn 재평가가 persona 기본 `theory_target`보다 session `theory_mode`를 우선하고, turn 재평가 `TurnContext`에도 같은 값을 채우도록 보강했다. CBT 체인·이론부합 루브릭은 후속. **검증: `python -B -m pytest -p no:cacheprovider app/test_orchestrator_masking.py app/test_session_turn_persistence.py -q` 31 passed, `C:\Users\encep\AppData\Local\Programs\Python\Python311\python.exe -X utf8 -m pytest -p no:cacheprovider app/test_eval_routes.py -q` 5 passed, `C:\Users\encep\AppData\Local\Programs\Python\Python311\python.exe -X utf8 -m pytest -p no:cacheprovider app/test_eval_routes.py app/test_notifications.py app/test_session_turn_persistence.py -q` 39 passed, `npm run typecheck`, `npm run build`, `npx playwright test e2e/session-layout.spec.ts --project=chromium-desktop --workers=1` 4 passed, `npx playwright test e2e/session-layout.spec.ts --project=chromium-mobile --workers=1` 4 passed, `npx playwright test e2e/session-mvp.spec.ts --project=chromium-single-run --workers=1` 1 passed, `npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run --workers=1` 9 passed.**
- M3 인증 claim 1차: Google/SAML/dev-login이 `AUTH_EMAIL_COHORT_MAP`·`AUTH_DOMAIN_COHORT_MAP` 및 SAML cohort claim을 `cohort_ids`로 전달하고, DB `app_user.external_id`는 provider subject(`google:`/`saml:`/`dev:`) 기반으로 저장한다. 운영 SAML 서명검증, 기관 claim schema/test tenant, deprovisioning audit은 후속. **검증: `pytest app/test_auth_providers.py -q` 30 passed, `pytest app/ -q` 178 passed.**
- X2 예산 경고/저비용 평가 라우팅/evaluator cache 관측·일별 비용 추이·모델별 비용 검증 리포트 2차: `ADMIN_USAGE_BUDGET_USD` 설정값을 기준으로 `GET /admin/usage``budget.status=disabled|ok|warn|exceeded`, 사용률, 잔여 예산을 반환하고 관리자 콘솔이 예산 상태 배너를 표시한다. 80% 이상 warn, 100% 이상 exceeded. `EVALUATOR_FAST_MODEL`/`EVALUATOR_DEEP_MODEL` 설정 시 fast/deep 평가 호출만 해당 모델 override로 gateway에 전달하고, 비워두면 기존 gateway default 라우팅을 유지한다. fast/deep evaluator structured 결과는 canonical request SHA-256 기반 인메모리 semantic cache로 재사용하며, 원문 prompt·completion은 저장하지 않고 성공 파싱 결과만 TTL/entry 제한 안에서 캐시한다. `/admin/usage`와 관리자 `AI 비용` 카드는 cache enabled/entries/hits/misses/stores/evictions/requests/hit_rate와 일별 `daily_cost` 추이를 표시하되 cache key·prompt·completion은 노출하지 않는다. `app.services.usage_report``scripts/report-ai-usage.py`는 동일 usage JSON에서 provider/model별 cost share, token share, cost/turn, cost/1k tokens, metered coverage, budget/cache warning을 산출한다. 자동 차단·한도 enforcement 정책은 후속. **검증: `python -B -m py_compile app/services/usage_report.py app/test_usage_report.py ..\..\scripts\report-ai-usage.py`, `python -B -m pytest -p no:cacheprovider app/test_usage_report.py app/test_evaluator_model_routing.py app/test_runtime_policy.py app/test_admin_ops.py -q` 42 passed, `scripts/report-ai-usage.py` sample CLI schema `vignette.ai_usage_model_cost_report.v1` 생성, prior `npm run check:api-types`, `npm run typecheck`, `npm run build`, admin E2E evidence remains valid because API response shape was not changed.**
- H4 LLM call audit/마스킹 2차: 상담 생성(generate/stream)·fast-loop 평가·deep-loop 평가의 외부 LLM 호출 직후 `audit.llm_call_log`에 provider/model/token/cost/inference_geo/latency metadata만 적재한다. prompt/completion 본문은 저장하지 않고 감사 실패는 상담 루프를 막지 않는다. 2026-06-28에는 한국어 이름/기관 로컬 휴리스틱 마스킹 1차를 추가했고, fast evaluator prompt의 내담자 응답과 client turn `text_masked`도 마스킹본을 쓰게 보강했다. 이어서 `app.services.pii_masking_eval`, `data/privacy/pii-masking-ko-fixtures.json`, `scripts/evaluate-pii-masking.py`로 합성 fixture 15케이스 평가 harness를 추가했고, `data/privacy/pii-masking-eval-input.schema.json``data/privacy/pii-masking-eval-report.schema.json`으로 source/category/severity metadata와 summary-only `technical_dry_run` report 계약을 고정했다. 66차에서는 `제 이름은 김서연입니다`, `보호자 이름은 박민수입니다`, `저는 최하늘입니다` 자연 발화형 이름 라벨·자기소개 케이스와 `이름은 중요하지 않다` negative control을 추가해 라벨 단어만 마스킹하고 실명을 남기던 구멍을 막았다. 70차에서는 `guardrail.mask_pii()` 내부에 선택형 한국어 PII recognizer adapter 경계를 추가했다. adapter는 import-time hard dependency가 아니며 명시 등록 전에는 비활성이고, 실패해도 기존 regex fallback이 마지막 안전망으로 유지된다. fake adapter 테스트는 regex가 못 잡는 별명/기관 span을 `[NAME]`/`[ORG]`로 마스킹하고 같은 문장의 전화번호는 후단 regex가 `[PHONE]`으로 처리하는지, adapter 실패 시에도 fallback이 유지되는지 검증한다. span 중복 처리는 문자별 set 할당 대신 정렬된 `last_end` 비교로 정리했다. 기본 CLI JSON은 `masked_text`/`forbidden_remaining` 원문 증거를 제외하며, `소속`/`안내`/`이름` NAME 오탐도 stopword로 보정했다. 로컬 dev-login 실제 `/turn` smoke에서 `audit.llm_call_log`가 8→11로 3행 증가했다(session `2460d56c-a9cb-4a40-a175-9575d510a5e9`). 공개 Google OAuth 실제 `/turn` proof는 별도 B2 항목에 남긴다. **검증: `py -3.11 -X utf8 -m py_compile apps/api/app/services/guardrail.py apps/api/app/services/pii_masking_eval.py apps/api/app/test_pii_masking_eval.py apps/api/app/test_orchestrator_masking.py scripts/evaluate-pii-masking.py`, `py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_pii_masking_eval.py app/test_orchestrator_masking.py app/test_evaluation_persistence.py app/test_session_turn_persistence.py -q` 47 passed, `py -3.11 -X utf8 scripts/evaluate-pii-masking.py --json` 15/15 pass, 과거 전체 기준 `pytest app/ -q` 119 passed.**
- H4 온보딩·동의 게이트 3차: `app_user`에 이름·소속·학과·학년/직위·전화번호·주소/수령지·닉네임·자기소개·아바타 URL·약관/개인정보 동의 시각·버전 필드를 추가했다. 로그인 직후 `/onboarding` 완료 전에는 전역 `OnboardingGate``/onboarding` 외 모든 앱 URL(`/learn`, `/settings`, `/admin`, `/dev/avatar-preview`, `/login` 포함)을 온보딩으로 이동시키고, 온보딩 중에는 공용 셸 메뉴 없이 가입 정보 입력 폼만 보여준다. learner `POST /sessions`와 voice dev persona 시작은 `onboarding_required`를 먼저 확인하고, 온보딩 저장 시 learner `consent_at`도 함께 세팅한다. 아바타는 `/users/me/avatar`에서 PNG/JPG/WebP MIME·시그니처·3MB 제한을 통과한 파일만 `USER_UPLOAD_DIR/profile-avatars`에 저장하고 DB에는 URL만 보관한다. 약관·개인정보 처리방침은 개인정보 보호법·처리방침 작성지침·안전성 확보조치 기준·약관규제법 취지를 반영한 개발 초안으로 `/users/legal-docs`에서 제공한다. 관리자/교수자 권한은 온보딩 화면에서 신청받지 않고 기존 `AUTH_ADMIN_EMAILS`/`AUTH_TEACHER_EMAILS` allowlist 및 관리자 사용자 관리 경로로만 부여한다. 한국어 이름/기관은 로컬 휴리스틱 마스킹, optional ko recognizer adapter 배선, 합성 fixture 평가 harness까지 완료했고, 실제 ko recognizer 모델/provider 선정, 운영 말뭉치 기반 오탐/미탐 평가, guardian/legal 최종 서명 동의서 evidence, 공개 Google OAuth 실제 `/turn` proof는 후속. **검증: `pytest app/ -q` 178 passed, `pytest engine_gateway/ -q` 11 passed, `npm run check:api-types`, `npm run typecheck`, `npm run build`, `npx playwright test e2e/auth.spec.ts --project=chromium-desktop --workers=1` 7 passed, `PLAYWRIGHT_PORT=5174 npm run e2e` 113 passed.**
- RAG warm 동시성 안정화: layout/session E2E가 여러 세션을 빠르게 만들 때 BGE-M3 embedder가 동시에 여러 번 지연 로드되며 `tqdm` lock 예외와 API health/dev-login timeout이 반복됐다. `rag.py`에서 embedder load/encode를 process-wide `RLock`으로 직렬화하고, `sessions.py``_warm_rag_caches`를 semaphore 1개로 제한해 warm task가 상담 요청 경로를 막지 않도록 했다. **검증: `pytest app/ -q` 125 passed, `npx playwright test e2e/layout-visual-gate.spec.ts e2e/session-layout.spec.ts --project=chromium-single-run --project=chromium-desktop --workers=1` 11 passed.**
- 공개/로컬/Tailnet 로그인 복구: public API 530 원인은 prod에서 개발 전용 `VIGNETTE_VOICE_POC_SAMPLE_TTS=true`가 fail-close된 것과 DB `app.admin_engine_config` 기본 행 부재였다. 2026-06-27 모바일 502는 cloudflared 로그의 `127.0.0.1:8001` origin refused와 일치했다. `scripts/start-public-runtime.ps1`는 public prod 기동 시 샘플 TTS를 강제로 끄고, 운영 DB에는 `claude_cli`/`127.0.0.1:9099` engine config 행을 복구했다. 추가로 dev/Tailnet에서는 public OAuth callback이 로컬/Tailnet 세션으로 붙지 않으므로 Google 버튼과 직접 시작 URL을 `local_oauth_unavailable`로 차단하고 dev-login만 사용한다. public OAuth state는 HttpOnly 쿠키에 묶인 HMAC 서명 토큰으로 복구해 API 재시작 뒤 콜백이 `invalid_state`로 떨어지지 않는다. callback 실패는 비밀값 없는 reason/status로 남기고, 로그인 화면도 token/state/provider/identity 실패 메시지와 reason code를 분리한다. 2026-06-27 18:57 KST에 Tailnet stale Vite `allowedHosts` 재발 상태를 재확인했고, `alpaca-home.taile93291.ts.net`를 Vite 기본 허용 host에도 포함해 수동 Vite 기동 시 403 재발 가능성을 낮췄다. **검증: `https://vignette.chanpaca.net/login` 200, `https://api-vignette.chanpaca.net/health` prod/db/engine true, public Google auth redirect 302 + HttpOnly state cookie, provider callback error → `/login?oauth=access_denied`, `https://alpaca-home.taile93291.ts.net/login` 200 + `/api/health` dev/db/engine true + `/api/auth/config` 200, env 없는 임시 Vite host-header smoke 200, local/Tailnet Google direct는 `local_oauth_unavailable`, local auth E2E 7 passed, Tailnet auth/dev-login E2E 2 passed.**
- Docker dev DB 안전장치: `scripts/dev-up.ps1``vignette-dev-db` healthcheck/pg_isready/role safety를 점검하고, 새 컨테이너 생성 시 `vignette_owner`와 앱 role을 분리해 healthcheck를 붙인다. `infra\.env` compose 필수값 누락도 사전 경고한다. `scripts/dev-down.ps1`은 기본 DB 보존, `-Db` 지정 시 컨테이너 중지로 정리했다. **검증: 현재 Docker DB accepting connections, `vignette_app:false:false`, 기존 컨테이너 healthcheck 없음. 패키징 smoke에서 fresh compose DB는 owner/app role 분리 상태로 API healthy까지 통과했다. 기존 dev DB는 `POSTGRES_USER=vignette` 기반이라 운영 role 전환은 별도 owner migration 확인 후 진행한다.**
- Docker 패키징 정리 1차: API Dockerfile은 저장소 루트 컨텍스트에서 `apps/api``data`를 함께 복사하고, 런타임 코드의 `Path(__file__).parents[...]` 루트 계산은 `app.paths.repo_root()`로 모았다. 웹 Dockerfile은 npm lockfile 기반 `npm ci`/`npm run build`로 고정하고, web/root `.dockerignore`로 host `node_modules`와 런타임 산출물이 이미지에 섞이지 않게 했다. 구현 없는 `rag.server` 사이드카는 compose에서 제거하고, RAG 무거운 의존성은 `INSTALL_RAG=true` build arg로 API 이미지에 opt-in 설치한다. 사용자 업로드는 `apiuploads` 볼륨(`/app/uploads`)으로 분리했다. API 이미지에 로컬 `apps/api/.env`가 복사되어 prod fail-closed 설정을 오염시키던 보안 결함도 `.dockerignore`로 차단했다. API startup은 init-complete DB에서 owner 권한 DDL을 건너뛰고, non-dev에서 schema가 미완성이면 owner-run init/migration과 `scripts/check-deploy-preflight.py`를 요구한다. `admin_engine_config` 행이 없으면 env 기본 엔진 설정으로 기동한다. backend base/RAG requirements는 exact version으로 고정했고, `data/kb` live-coaching source pack은 commit 또는 동일 경로 provision이 필요한 런타임 입력으로 문서화했다. **검증: `docker compose -f infra/docker-compose.yml config --quiet` 더미 필수 env 통과, API/Web image build 통과, API image `.env*` 미포함, prod import smoke 통과, fresh `docker compose up -d --build` smoke 통과(`/api/health` 200, `db:true`, `engine:true`, `api healthy`), `python scripts/check-deploy-preflight.py --skip-db --env-file infra\.env.example --allow-placeholder-secrets` 통과. 실제 이식 전에는 배포지 `infra/.env``APP_DB_PASSWORD`, OAuth client id/secret, `OPENAI_API_KEY`, `SESSION_SECRET` 등 소유자 비밀값을 채워야 한다.**
- [ ] **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

@ -4,6 +4,7 @@
> 방법: 5개 차원 병렬 분석(데드코드·과다파라미터·중복·SSOT·추상화) + 방법론 연구 → 종합(워크플로우 code-quality-research).
> 주의: 분석의 'location/참조 0건'은 착수 전 코드 재확인 권고. 자기수정 포함(예: make_eval_hook은 sessions 경로 주입 완료, voice 경로만 누락).
> 2026-06-27 현행화: voice 경로도 `eval_hook=evaluator.make_eval_hook(engine_client)` 주입 완료. fast-loop 평가는 `TurnRecord.evaluation`에 더해 `app.feedback_scores`/`turn_technique`/`turn_client_state` 등 DB 정규화 적재·복원 경로가 추가됐다. 아래 표의 voice eval_hook 누락 항목은 과거 기록으로만 본다.
> 2026-07-02 현행화: 3.4-2 **Stage enum 중복 해소**`taxonomy.Stage`를 라벨 단일 정의(SoT)로 두고 `services.state_machine`이 re-export(중복 `class Stage(str,Enum)` 제거, 순환 import 없음). 검증: 단일 enum 정체성(`state_machine.Stage is taxonomy.Stage`), 수집 379 import 무결, 백엔드 352 passed. 3.3-1~3(턴 파이프라인 dead-dup)·3.5-1(tier)·3.5-4(eval_hook 주입)는 이전 라운드에 이미 해소됨. 이번 라운드 추가 해소: **3.3-6** store `recent_turns`/`masked_turns` Type-2 중복 → `masked_turns()[-k:]` 위임, **3.3-8/3.4-3(부분)** 화자 한글라벨 4× 삼항 → `taxonomy.speaker_ko_label` 단일 헬퍼. **3.5-5** `_list_sessions` N+1 제거 — (1차) 세션별 `session_state`/`turns` fetch 루프(1+2N) → `= ANY($ids)` 배치 3쿼리 + Python 그룹핑, (2차) 상세뷰 턴평가 하이드레이션도 세션별 evaluator 연결·로드 → 전 세션 turn_refs 배치(연결 1회). 검증: 실 DB 동등성 50세션/109턴 불일치 0, 왕복 101→3, 백엔드 352 passed(회귀 mock 시퀀스 갱신). **3.4-3/3.4-5** Speaker/Role 계약 레벨은 실측 결과 조치 불요(DB actor 매핑 1곳·`DB_ROLE_BY_APP`/`APP_ROLE_BY_DB` 이미 단일 dict·`AIRole` 단일 Literal). **→ 그룹 E의 클린·행위보존·테스트가능 항목 소진.** 잔여는 소유자 결정(eval_hook 정책)·미래(Node 게이트웨이 구현 시)·인프라(migration runner)로 코드 리팩터 성격이 아니다.
---