현재 작업 전체 반영

This commit is contained in:
Yun Chan 2026-06-27 16:08:41 +09:00
parent 5560638e54
commit c0dddab594
85 changed files with 11322 additions and 539 deletions

View file

@ -1,6 +1,6 @@
# Vignette Handoff
> Updated: 2026-06-26 15:18 KST. 새 세션은 이 문서와 `docs/DESIGN_CONCEPT.md`를 먼저 읽고 이어가면 된다.
> Updated: 2026-06-27 15:54 KST. 새 세션은 이 문서와 `docs/DESIGN_CONCEPT.md`를 먼저 읽고 이어가면 된다.
## 현재 상태
@ -10,15 +10,26 @@
- 로컬 engine gateway 기본 포트: `http://127.0.0.1:9099`
- 공개 웹: `https://vignette.chanpaca.net`
- 공개 API: `https://api-vignette.chanpaca.net`
- 앱 배포 소스 commit: `543d48c` (`Block legacy Live2D asset routes`)
- 최신 Cloudflare Pages production deploy: `0fe8f3cc`, source `543d48c`, branch `main`
- 앱 배포 소스 commit: `6bd91b0d` (`현재 작업 상태 저장`)
- 최신 Cloudflare Pages production deploy: `2a58ca49`, source `6bd91b0d` + dirty worktree, branch `main`
- Google OAuth 허용 이메일 도메인: `hs.ac.kr`, `twentyoz.kr`
- 최신 백엔드 회귀: `python -m pytest app/ -q``119 passed`
- X1 재귀학습 export 1차: `scripts/export-recursive-dataset.py` 기본 read-only dry-run, `--write-dataset` 명시 시에만 `ds.*` write, approved export는 steward/legal/IAA gate 없으면 거부.
- C1 사례개념화 워크시트 1차: `SessionReviewResponse.caseWorksheet`와 리뷰 화면 read-only 카드가 축어록 근거 기반 초안을 제공한다. 편집·DB 저장, 임상 루브릭, AI 추출/채점은 후속.
- C3 이론모드 1차: `theory_mode`가 세션·평가·생성 프롬프트까지 흐르고, `build_turn_messages`는 인간중심/CBT/통합 프레이밍을 엔진 메시지에 넣는다. CBT 체인·이론부합 루브릭·명시적 선택 UI는 후속.
- H2 평가 정규화: 턴 평가의 대안발화도 `app.alternative_utterance`에 적재하고 리뷰 hydrate 시 `alternative_utterances`로 복원한다.
- X2 예산 경고: `ADMIN_USAGE_BUDGET_USD`가 0보다 크면 `/admin/usage`가 budget 상태(ok/warn/exceeded)를 반환하고 `/admin`이 예산 배너를 표시한다. 0 또는 미설정이면 disabled.
- H4 LLM call audit: 상담 생성(generate/stream)과 fast/deep 평가의 외부 LLM 호출 직후 `audit.llm_call_log`에 provider/model/token/cost/inference_geo/latency metadata만 적재한다. prompt/completion 본문은 저장하지 않는다. 실제 운영 `/turn` 후 live row 확인은 아직 필요하다.
- M3 인증 claim 1차: Google/SAML/dev-login이 설정 기반 cohort map과 SAML cohort claim을 `cohort_ids`로 넘기고, 관리 사용자 `external_id`는 provider subject 기반(`google:`/`saml:`/`dev:`)으로 저장한다. 운영 SAML 서명검증·기관 claim schema·deprovisioning audit은 후속.
- Google OAuth 진단 1차: provider callback error는 `access_denied`/`provider_error`로 분리하고, 로그인 화면은 실패 reason code를 함께 표시한다. 실제 Google 계정 완료 proof는 아직 owner 로그인/storageState가 필요하다.
- `frontenddesign` 스킬은 현재 세션의 사용 가능 스킬 목록에 없었다. 대신 `docs/DESIGN_CONCEPT.md`를 SSOT로 사용했다.
- 공개 API 터널은 현재 `C:\Users\encep\.cloudflared\vignette-config.yml`에서 `http://127.0.0.1:8001`을 본다.
- 공개용 API 프로세스는 `127.0.0.1:8001`에서 `ENVIRONMENT=prod`로 떠 있다. 로컬 개발 API `127.0.0.1:8000`은 현재 내려 두었다.
- 공개용 API 프로세스는 `127.0.0.1:8001`에서 `ENVIRONMENT=prod`로 떠 있다. 로컬 개발 API `127.0.0.1:8000`과 Tailnet 개발 API `127.0.0.1:8010``ENVIRONMENT=dev`로 떠 있다.
- 2026-06-27 05:34:32 UTC 모바일 502는 cloudflared 로그상 `127.0.0.1:8001` origin connection refused와 일치한다. 현재 public API는 복구됐고, watchdog 기본 검사에서 아직 DNS가 없는 `api-vnet.18ka.net`을 제외해 향후 설치 시 불필요한 restart loop를 막았다.
- `GET /personas`는 이제 인증 필요다. 검증 당시 로컬 `http://127.0.0.1:8000/personas`와 현재 공개 `https://api-vignette.chanpaca.net/personas` 모두 비로그인 401 확인 완료.
- 로컬 웹이 `http://127.0.0.1:5175`처럼 다른 Vite 포트로 떠도 로그인은 `로컬 테스트 계정으로 계속` 버튼을 사용한다. Google OAuth 버튼은 로컬에서는 disabled로 둔다. 현재 OAuth callback이 공개 API로 돌아가기 때문에 로컬 Google OAuth는 로컬 세션에 붙을 수 없다.
- 중복 Vite/API/gateway/cloudflared 프로세스는 정리했다. 현재 확인용 프로세스는 `127.0.0.1:9099` engine gateway, `127.0.0.1:8001` prod public API, `cloudflared` tunnel 1개뿐이다. `127.0.0.1:8000` dev API와 임시 Vite preview는 내려 두었다. Playwright 전체 테스트는 자체 Vite web server를 `127.0.0.1:5173`에 띄워 검증했다.
- 현재 확인용 프로세스는 `127.0.0.1:9099` engine gateway, `127.0.0.1:8001` prod public API, `127.0.0.1:8000` dev API, `127.0.0.1:8010` Tailnet dev API, `127.0.0.1:5173` Vite web, `cloudflared` tunnel 1개다. Tailnet URL은 `https://alpaca-home.taile93291.ts.net/login`이며 dev-login까지 검증됐다.
- Docker `vignette-dev-db`는 실행 중이고 DB는 accepting connections다. 단, 기존 컨테이너라 healthcheck가 없고 `POSTGRES_USER=vignette` 기반이다. `vignette_app` role은 NOBYPASSRLS로 존재하지만 현재 API startup DDL이 owner 권한을 요구하므로 런타임 app-role 전환은 마이그레이션 owner/런타임 role 분리 후 진행한다.
- 공개 런타임 재기동 스크립트:
- start: `powershell -NoProfile -ExecutionPolicy Bypass -File scripts\start-public-runtime.ps1`
- stop: `powershell -NoProfile -ExecutionPolicy Bypass -File scripts\stop-public-runtime.ps1`
@ -112,8 +123,7 @@
- `cd apps/web; fnm use 22.22.3; npm run build`
- Passed
- Docker compose config:
- `cd infra`
- With dummy required env vars, `docker compose --env-file .env.example config --quiet`
- With dummy required env vars, `docker compose -f infra\docker-compose.yml --env-file infra\.env.example config --quiet`
- Passed
- Focused Playwright after fixes:
- `npx playwright test e2e/admin.spec.ts e2e/db-persistence.spec.ts e2e/voice-success.spec.ts --project=chromium-desktop --project=chromium-mobile --project=chromium-single-run`
@ -144,13 +154,19 @@
- `powershell -NoProfile -ExecutionPolicy Bypass -File scripts\start-public-runtime.ps1`
- API 8001 health and public `https://api-vignette.chanpaca.net/health` both returned `environment:"prod"`, `db:true`, `engine:true`
- Public web deployment:
- `wrangler pages deploy dist --project-name vignette --branch main --commit-hash 543d48c`
- Production deployment `0fe8f3cc`
- `https://vignette.chanpaca.net/` serves `assets/index-D1Z5DH5J.js` and `assets/index-BA9-xxfn.css`
- `wrangler pages deploy dist --project-name vignette --branch main --commit-dirty=true`
- Production deployment `2a58ca49`
- `https://vignette.chanpaca.net/` serves `assets/index-BzQmv-jU.js` and `assets/index-CCZ1bDA9.css`
- `https://vignette.chanpaca.net/live2d/mao/Mao.model3.json`, Haru model, and Cubism core routes return 404
- Public login screen:
- `npx playwright test e2e/auth.spec.ts --project=chromium-desktop --grep "public login"`
- `1 passed`
- OAuth state is now HMAC-signed and bound to a HttpOnly state cookie; fake callback with state cookie now reaches `oauth=token_exchange_failed` instead of `oauth=invalid_state`, provider callback error redirects to `oauth=access_denied`, and callback failures log non-secret reason/status details in `api.public.err.log`.
- Login UI maps known OAuth/SAML failure reasons and displays `오류 코드: ...`; auth focused E2E is now 7 passed.
- Public runtime watchdog:
- `powershell -NoProfile -ExecutionPolicy Bypass -File scripts\watch-public-runtime.ps1 -CheckOnly`
- healthy: `engine, api, web-preview, cloudflared, public-api`
- `api-vnet.18ka.net` is not part of the default watchdog checks while DNS is not live; add it later with `-AdditionalPublicHealthUrls`.
- Local login on `127.0.0.1:5175`:
- `PLAYWRIGHT_BASE_URL=http://127.0.0.1:5175 PLAYWRIGHT_SKIP_WEB_SERVER=1 npx playwright test e2e/auth.spec.ts --project=chromium-desktop`
- `6 passed`

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

View file

@ -0,0 +1,13 @@
Use case: subtle full-bleed website background texture for the Vignette clinical counseling training app.
Create a raster bitmap background only. No text, no UI, no screens, no device mockups, no logo, no people, no tile grid, no collage panels, no isolated sticker sheet.
Visual direction: a continuous warm off-white handmade paper surface with soft daylight, very subtle sage-teal and muted terracotta watercolor stains, faint botanical line impressions, light notebook-paper grain, and calm clinical education atmosphere. The image should work behind real interface panels without demanding attention.
Composition and framing: 16:9 landscape, seamless single surface, clean corners, generous negative space, no visible borders, no discrete cards, no obvious central object, no dramatic blob gradients.
Style and medium: refined editorial raster texture, warm clinical SaaS background, soft natural material, photographic-paper-meets-watercolor feel. Not vector art, not SVG, not flat icon illustration.
Palette: warm paper (#FBFAF8, #F4F2EE), sage teal (#3E7A6E), muted terracotta (#B0735C), low saturation, gentle contrast.
Output should be suitable as a CSS background image with panels layered over it.

View file

@ -0,0 +1,109 @@
param(
[int]$Concurrency = 3
)
$ErrorActionPreference = "Stop"
$repo = "D:\workspace\vignette"
$bashExe = "C:\Program Files\Git\bin\bash.exe"
$wrapper = "/c/Users/encep/.codex/imagegen-headless/codex_imagegen.sh"
$runner = "/d/workspace/vignette/docs/design-concepts/prompts/run-one-imagegen.sh"
$outDir = "/d/workspace/vignette/docs/design-concepts/generated"
$promptDoc = Join-Path $repo "docs\design-concepts\prompts\vignette-design-prompts-2026-06-27.md"
$runtimePromptDir = Join-Path $repo "docs\design-concepts\prompts\.runtime"
$items = @(
@{
Slug = "01-login-responsive"
Out = "$outDir/01-login-responsive.png"
},
@{
Slug = "02-learner-home-responsive"
Out = "$outDir/02-learner-home-responsive.png"
},
@{
Slug = "03-session-responsive"
Out = "$outDir/03-session-responsive.png"
},
@{
Slug = "04-session-review-responsive"
Out = "$outDir/04-session-review-responsive.png"
},
@{
Slug = "05-professor-responsive"
Out = "$outDir/05-professor-responsive.png"
},
@{
Slug = "06-admin-responsive"
Out = "$outDir/06-admin-responsive.png"
},
@{
Slug = "07-settings-responsive"
Out = "$outDir/07-settings-responsive.png"
},
@{
Slug = "08-warm-visual-elements"
Out = "$outDir/08-warm-visual-elements.png"
},
@{
Slug = "09-avatar-pattern-elements"
Out = "$outDir/09-avatar-pattern-elements.png"
}
)
function Get-PromptBlock {
param([string]$Slug)
$raw = Get-Content -Raw -Encoding UTF8 -LiteralPath $promptDoc
$pattern = "(?ms)^### $([regex]::Escape($Slug))\s*(.*?)(?=^### |\z)"
$match = [regex]::Match($raw, $pattern)
if (-not $match.Success) {
throw "프롬프트 블록을 찾지 못함: $Slug"
}
return $match.Groups[1].Value.Trim()
}
New-Item -ItemType Directory -Force -Path (Join-Path $repo "docs\design-concepts\generated") | Out-Null
New-Item -ItemType Directory -Force -Path $runtimePromptDir | Out-Null
$queue = New-Object System.Collections.Queue
foreach ($item in $items) {
$queue.Enqueue($item)
}
$running = @()
while ($queue.Count -gt 0 -or $running.Count -gt 0) {
while ($queue.Count -gt 0 -and $running.Count -lt $Concurrency) {
$item = $queue.Dequeue()
$prompt = Get-PromptBlock -Slug $item.Slug
$promptFileWin = Join-Path $runtimePromptDir "$($item.Slug).txt"
$promptFileBash = "/d/workspace/vignette/docs/design-concepts/prompts/.runtime/$($item.Slug).txt"
$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
[System.IO.File]::WriteAllText($promptFileWin, $prompt, $utf8NoBom)
Write-Host "[imagegen] start $($item.Slug)"
$job = Start-Job -Name $item.Slug -ScriptBlock {
param($BashExe, $BashRunner, $BashWrapper, $OutPath, $PromptFile)
$ErrorActionPreference = "Continue"
& $BashExe $BashRunner $PromptFile $BashWrapper $OutPath
if ($LASTEXITCODE -ne 0) {
throw "imagegen failed: $OutPath"
}
} -ArgumentList $bashExe, $runner, $wrapper, $item.Out, $promptFileBash
$running += $job
}
$done = Wait-Job -Job $running -Any -Timeout 15
if ($null -eq $done) {
continue
}
foreach ($job in @($done)) {
Receive-Job -Job $job
if ($job.State -ne "Completed") {
throw "이미지 생성 작업 실패: $($job.Name)"
}
Write-Host "[imagegen] done $($job.Name)"
Remove-Job -Job $job
$running = @($running | Where-Object { $_.Id -ne $job.Id })
}
}
Write-Host "[imagegen] all done"

View file

@ -0,0 +1,9 @@
#!/usr/bin/env bash
set -eo pipefail
prompt_file="$1"
wrapper="$2"
out_path="$3"
prompt="$(cat "$prompt_file")"
"$wrapper" --out "$out_path" --size "2048x1152" --quality "medium" --prompt "$prompt" 2>&1

View file

@ -0,0 +1,143 @@
# Vignette 디자인 시안 이미지 생성 프롬프트
작성일: 2026-06-27
목적: 주요 화면의 실제 기능을 기준으로 모바일, 태블릿, PC 반응형 디자인 시안을 만든다. 결과 이미지는 구현 확정안이 아니라 `docs/dev_dashboard.html`에 넣을 디자인 지침과 시각 방향 후보로 쓴다.
공통 방향:
- 컨셉: 불안한 사람을 다루는 사람을 위한, 차분한 임상 도구.
- 톤: 65% 임상 신뢰, 35% 교육적 따뜻함.
- 팔레트: warm off-white `#FBFAF8`, ink green `#1C2A2A`, sage teal `#3E7A6E`, muted terracotta `#B0735C`, slate blue info.
- 형태: 6-12px radius, hairline border, 아주 약한 그림자. 과한 글로우, 보라/블루 그라데이션, 랜딩페이지식 카드 덤프 금지.
- 이미지 조건: 반드시 래스터 UI 이미지. SVG, 벡터, 와이어프레임, 로고 시트, 장식용 그라데이션 오브 금지.
- 시트 구성: 각 화면은 한 장의 16:9 raster concept sheet 안에 PC, tablet, mobile 3개 디바이스 목업을 함께 보여준다.
## 화면별 기능 추출
| 화면 | 핵심 기능 | 시각 의도 |
|---|---|---|
| 로그인/진입 | Google OAuth, 승인 도메인 안내, 로컬 테스트 역할 선택, 교육용 비치료 고지 | 신뢰와 보안이 먼저 보이는 조용한 진입 화면 |
| 학습자 홈 | 내담자 선택, 주호소/난도/접근/음성 정보, 새 회기 시작, 진행 중/리뷰 가능/누적 회기, 기존 회기 이어가기 | 오늘 시작할 연습이 명확한 상담 연습 작업대 |
| 상담 세션 | 회기 단계, 내담자 컨텍스트, 반구상 아바타, 음성 오브, 실시간 자막, 텍스트 폴백, 몰입/상태신호/코칭 모드, 안전 점검 | 화면 중심은 내담자, 피드백은 조용한 신호 |
| 회기 리뷰 | 한 문장 요약, 감정 밸런스 타임라인, 회기 흐름, 축어록, 기법 태그, 좋았던 순간, 개선점, 오디오/PDF | 점수 폭격이 아니라 근거 기반 회고 |
| 교수 콘솔 | 리뷰 대기 큐, 담당 코호트 현황, 페르소나 검수 승인/반려, 종료 회기 목록, 최근 회기 테이블 | awareness가 아니라 triage |
| 관리자 콘솔 | 서비스 상태, 엔진/DB/음성/리뷰 리소스, 사용자 등록/수정/비활성화, 검색, 운영 저장소 상태 | 시스템 건강성과 권한 관리를 한 화면에서 |
| 설정 | 계정, 관리자 AI 운영 설정, 테마, 음성 프리셋, 말하기 속도, 알림 | 역할별 설정만 보이는 조용한 환경 관리 |
| 온화한 이미지 요소 | 종이 질감, 상담실의 책상/노트/식물/차분한 빛, 임상 교육 도구의 배경 요소 | UI 뒤에서만 느껴지는 따뜻함 |
| 반구상 아바타/패턴 요소 | 교육용 가상 내담자 카드, 부드러운 광배, 정서 파츠 느낌, 배경 패턴 | 실사 오인과 유아적 일러스트를 모두 피함 |
## 생성 프롬프트
### 01-login-responsive
Use case: ui-mockup
Asset type: responsive product UI concept sheet for Vignette login
Primary request: Create a polished raster UI concept sheet showing the Vignette login entry page in three device mockups: desktop PC, tablet, and mobile, side by side on one 16:9 canvas.
Screen functions: Google OAuth login, allowed school domain notice, local test role selector for learner teacher admin, secure session copy, education-only non-treatment notice.
Style/medium: realistic product UI screenshot mockup, raster bitmap image, calm Korean clinical education SaaS interface, not a wireframe.
Composition/framing: left desktop mockup largest, tablet middle, mobile right; each device shows the same page adapted responsively. Desktop uses a dark muted stage panel on the left and login panel on the right; tablet stacks content; mobile compresses to a single clean column.
Visual elements: subtle warm paper texture, small security shield icon style, quiet school account entry, no decorative blobs.
Color palette: warm off-white #FBFAF8, ink green #1C2A2A, sage teal #3E7A6E, muted terracotta #B0735C, low-saturation slate.
Text hints: Korean UI labels such as "Vignette", "상담 시뮬레이션 학습", "학교 Google 계정으로 계속", "허용 도메인", "로컬 테스트".
Constraints: raster image only; no SVG; no vector art; no wireframe; no logo sheet; no purple gradient; no emoji; no watermark.
### 02-learner-home-responsive
Use case: ui-mockup
Asset type: responsive product UI concept sheet for learner home
Primary request: Create a polished raster UI concept sheet showing the learner home page in desktop PC, tablet, and mobile device mockups on one 16:9 canvas.
Screen functions: persona list, selected client preview, presenting problem summary, difficulty, counseling approach, voice preset, start new session button, active/review/total session stats, previous sessions with continue/review actions.
Style/medium: realistic product UI screenshot mockup, raster bitmap, quiet clinical training workspace.
Composition/framing: desktop shows a left persona list and wide selected-client workspace; tablet turns persona list into a two-column selector; mobile uses horizontal persona chips and compact practice card.
Visual elements: semi-figurative client initial card, warm paper surface, subtle counseling notebook image area, restrained data chips.
Color palette: warm off-white, sage teal accent, terracotta warmth, slate text, hairline dividers.
Text hints: Korean labels such as "오늘의 회기를 준비합니다", "연습 대상", "새 회기 시작", "기존 회기", "리뷰 가능".
Constraints: raster image only; no SVG; no vector art; no generic landing hero; no oversized decorative illustration; no emoji; no watermark.
### 03-session-responsive
Use case: ui-mockup
Asset type: responsive product UI concept sheet for live counseling session
Primary request: Create a polished raster UI concept sheet showing the live counseling session screen in desktop PC, tablet, and mobile mockups on one 16:9 canvas.
Screen functions: vertical phase tracker, client context, dark client stage, semi-figurative avatar bust, voice orb, real-time transcript, text fallback composer, live signal rail, client state meters, safety check, bottom control bar with mic pause feedback mode and slide-to-end.
Style/medium: realistic product UI screenshot mockup, raster bitmap, immersive clinical training tool.
Composition/framing: desktop has 3-column counseling room layout; tablet collapses side panels into top context and bottom controls; mobile shows avatar stage, transcript, compact signal strip, and large thumb-friendly input.
Visual elements: soft raster avatar bust with 35-45 percent realism, gentle breathing light aura, dark muted charcoal-green stage, warm notebook texture in panels.
Color palette: dark stage #1E2724, sage teal, terracotta for learner voice, off-white panels, muted slate blue info.
Text hints: Korean labels such as "회기 진행", "실시간 자막", "라이브 신호", "관찰 신호", "몰입", "상태 신호", "코칭".
Constraints: raster image only; no SVG; no vector art; no sci-fi dashboard; no red warning decoration except small safety state; no emoji; no watermark.
### 04-session-review-responsive
Use case: ui-mockup
Asset type: responsive product UI concept sheet for session review
Primary request: Create a polished raster UI concept sheet showing the session review page in desktop PC, tablet, and mobile mockups on one 16:9 canvas.
Screen functions: review headline summary, session metadata, audio replay, PDF export, emotion valence timeline, phase flow, annotated transcript, technique labels, supervisor callouts, good moments, growth points, next utterance suggestion.
Style/medium: realistic product UI screenshot mockup, raster bitmap, reflective clinical education review surface.
Composition/framing: desktop has wide transcript column and right insight rail; tablet stacks charts above transcript; mobile uses tabs/sections and compact annotated turns.
Visual elements: line chart with minimal grid, transcript timeline nodes, calm supervisor note cards, subtle paper texture.
Color palette: off-white, sage teal for progress, slate blue for AI notes, amber for watch items, very restrained terracotta.
Text hints: Korean labels such as "회기 리뷰", "감정 밸런스 타임라인", "세션 트랜스크립트", "좋았던 순간", "다음 회기 개선점".
Constraints: raster image only; no SVG; no vector art; no radar chart; no score fireworks; no emoji; no watermark.
### 05-professor-responsive
Use case: ui-mockup
Asset type: responsive product UI concept sheet for professor console
Primary request: Create a polished raster UI concept sheet showing the professor console in desktop PC, tablet, and mobile mockups on one 16:9 canvas.
Screen functions: review queue, cohort session counts, persona review approval/rejection queue, ended sessions awaiting review, recent session table.
Style/medium: realistic product UI screenshot mockup, raster bitmap, calm instructor triage dashboard.
Composition/framing: desktop uses full-width triage strip plus left approval queue and right recent sessions table; tablet stacks queue then table; mobile shows prioritized cards and compact rows.
Visual elements: small risk dots, low-noise tables, persona approval cards, no alarmist styling.
Color palette: off-white, sage teal, muted slate blue, small controlled amber/red only for actual intervention.
Text hints: Korean labels such as "교수 콘솔", "검토 큐", "페르소나 검수", "검토 대기", "최근 세션".
Constraints: raster image only; no SVG; no vector art; no gamified leaderboard; no red-heavy dashboard; no emoji; no watermark.
### 06-admin-responsive
Use case: ui-mockup
Asset type: responsive product UI concept sheet for admin operations console
Primary request: Create a polished raster UI concept sheet showing the admin console in desktop PC, tablet, and mobile mockups on one 16:9 canvas.
Screen functions: service health summary, engine mode status, service resource rows, progress meters, user creation form, user search, user role/cohort editing, save/deactivate actions.
Style/medium: realistic product UI screenshot mockup, raster bitmap, restrained operations console for a clinical education platform.
Composition/framing: desktop uses status strip, service resource table, sidecar user creation, editable user list; tablet stacks sections; mobile uses collapsible service rows and full-width user cards.
Visual elements: health dots, quiet progress bars, compact forms, audit-friendly user rows.
Color palette: warm neutral surfaces, slate admin tone, sage teal focus, amber/deep red only for degraded/down states.
Text hints: Korean labels such as "운영 콘솔", "서비스 상태", "서비스 리소스", "사용자 관리", "사용자 등록".
Constraints: raster image only; no SVG; no vector art; no neon ops dashboard; no excessive charts; no emoji; no watermark.
### 07-settings-responsive
Use case: ui-mockup
Asset type: responsive product UI concept sheet for settings
Primary request: Create a polished raster UI concept sheet showing the settings page in desktop PC, tablet, and mobile mockups on one 16:9 canvas.
Screen functions: account profile, role badge, admin AI engine operation settings, theme toggle, voice preset selection, speech rate slider, notification toggles.
Style/medium: realistic product UI screenshot mockup, raster bitmap, calm settings workspace.
Composition/framing: desktop shows a left settings rail and right forms; tablet keeps rail compact; mobile turns sections into stacked panels with sticky section labels.
Visual elements: account avatar initial, segmented engine mode control, voice preset cards, sliders and toggles.
Color palette: off-white, sage teal, terracotta accent sparingly, slate text, hairline dividers.
Text hints: Korean labels such as "설정", "계정", "AI 운영", "테마", "음성", "알림".
Constraints: raster image only; no SVG; no vector art; no decorative app-store settings page; no emoji; no watermark.
### 08-warm-visual-elements
Use case: stylized-concept
Asset type: raster visual element direction board for Vignette UI backgrounds
Primary request: Create a warm raster image element board for the Vignette platform: subtle paper texture backgrounds, quiet counseling desk objects, notebook pages, soft plant shadow, warm indirect light, small non-distracting educational motifs.
Style/medium: soft bitmap editorial illustration mixed with realistic material texture, not vector, not SVG.
Composition/framing: 16:9 board with several rectangular crops showing background treatments and UI-safe negative space. No full UI screens, just visual materials.
Lighting/mood: quiet morning light, clinical but warm, non-decorative.
Color palette: warm paper, sage teal, muted terracotta, slate ink, low contrast.
Constraints: raster image only; no SVG; no vector art; no obvious stock-photo look; no people; no readable text; no watermark; no gradient blobs.
### 09-avatar-pattern-elements
Use case: stylized-concept
Asset type: raster visual direction board for semi-figurative client avatar and gentle pattern elements
Primary request: Create a raster visual direction board for educational virtual client avatars and supporting patterns for Vignette. Show semi-figurative bust cards with low realism, soft breathing aura backgrounds, expression part hints for eyes and mouth, and subtle paper patterns suitable for UI panels.
Style/medium: painterly raster UI illustration, warm clinical education tone, not vector, not SVG.
Composition/framing: 16:9 board with three avatar-card variations and several small pattern crops; generous margins; no actual production assets.
Lighting/mood: calm, empathetic, restrained.
Color palette: sage teal, terracotta, warm off-white, muted slate, natural skin tones with low saturation.
Constraints: raster image only; no SVG; no vector art; no photorealistic patient; no cute cartoon style; no mascot; no text except tiny UI-like labels; no watermark.

View file

@ -21,6 +21,7 @@
--mono:"JetBrains Mono",ui-monospace,"SFMono-Regular",monospace;
}
*{box-sizing:border-box;}
html{scroll-padding-top:76px;}
body{margin:0;background:var(--paper);color:var(--ink);font-family:var(--font);line-height:1.55;-webkit-font-smoothing:antialiased;}
a{color:var(--accent-deep);}
/* header */
@ -42,7 +43,7 @@
.sec-head{display:flex;align-items:flex-end;justify-content:space-between;gap:16px;margin:0 0 14px;}
.sec-head h2{margin:0;font-size:18px;line-height:1.3;letter-spacing:-.01em;}
.sec-head .sub{margin:5px 0 0;color:var(--ink-3);font-size:12.5px;max-width:780px;}
section.block{margin-top:34px;}
section.block{margin-top:34px;scroll-margin-top:76px;}
/* PULSE / hero */
.pulse{display:grid;grid-template-columns:minmax(0,1.05fr) minmax(330px,.95fr);gap:22px;align-items:stretch;margin-top:22px;}
.pulse-main{background:var(--surface);border:1px solid var(--hair);border-radius:16px;padding:22px 24px;box-shadow:0 1px 2px rgba(28,42,42,.05);}
@ -130,6 +131,23 @@
.ml-owner i{background:var(--clay-tint);border-color:var(--clay);}
.ml-crit i{background:#fff;border-color:var(--crit);}
.ml-gate i{background:var(--accent-tint);border-color:var(--accent);}
/* DESIGN GUIDE */
.dg-shell{background:linear-gradient(180deg,var(--surface),var(--paper) 100%);border:1px solid var(--hair);border-radius:16px;padding:18px;box-shadow:0 1px 2px rgba(28,42,42,.05);}
.dg-link{display:inline-flex;align-items:center;justify-content:center;min-height:34px;padding:8px 12px;border:1px solid color-mix(in srgb,var(--accent) 34%,var(--hair));border-radius:999px;background:var(--accent-tint);color:var(--accent-deep);font-size:12px;font-weight:750;text-decoration:none;white-space:nowrap;}
.dg-note{margin:0 0 14px;padding:12px 13px;border:1px dashed color-mix(in srgb,var(--clay) 38%,var(--hair));border-radius:11px;background:var(--clay-tint);color:var(--clay-deep);font-size:12.5px;line-height:1.55;}
.dg-principles{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;margin:0 0 16px;}
.dg-principles div{background:var(--surface);border:1px solid var(--hair);border-radius:12px;padding:12px 13px;}
.dg-principles b{display:block;margin-bottom:4px;font-size:12.5px;color:var(--ink);}
.dg-principles span{display:block;color:var(--ink-2);font-size:11.5px;line-height:1.5;}
.dg-table-wrap{overflow-x:auto;margin:0 0 16px;}
.dg-table{min-width:760px;margin:0;}
.dg-gallery{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px;}
.dg-card{min-width:0;background:var(--surface);border:1px solid var(--hair);border-radius:13px;overflow:hidden;box-shadow:0 1px 2px rgba(28,42,42,.04);}
.dg-card img{display:block;width:100%;aspect-ratio:1672/941;object-fit:cover;background:var(--paper-2);border-bottom:1px solid var(--hair);}
.dg-card-body{padding:12px 13px 13px;}
.dg-card b{display:block;margin-bottom:4px;font-size:13px;color:var(--ink);}
.dg-card span{display:block;color:var(--ink-2);font-size:11.5px;line-height:1.45;}
.dg-card code{display:inline-block;margin-top:8px;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
/* BOARD */
.filterbar{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin:0 0 16px;}
.filterbar .fl{font:700 10.5px/1 var(--mono);letter-spacing:.08em;text-transform:uppercase;color:var(--ink-3);margin-right:4px;}
@ -234,11 +252,13 @@
.pulse{grid-template-columns:1fr;}
.graphs{grid-template-columns:1fr;}
.ocols{grid-template-columns:1fr;}
.dg-principles,.dg-gallery{grid-template-columns:repeat(2,minmax(0,1fr));}
}
@media (max-width:760px){
header{padding:0 16px;} main{width:calc(100% - 28px);}
.kpis{grid-template-columns:1fr 1fr;}
.grid,.roadmap,.detail-head,.detail-blocks{grid-template-columns:1fr;}
.dg-principles,.dg-gallery{grid-template-columns:1fr;}
.task-row,.task-row.r3{grid-template-columns:1fr;}
.task-row>div{border-bottom:1px solid var(--hair);} .task-row>div:last-child{border-bottom:0;}
.brand small{display:none;}
@ -261,14 +281,14 @@
<section class="pulse">
<div class="pulse-main">
<span class="eyebrow">project pulse</span>
<h1>지금 위치: <em>Phase 0 마감 직전</em> → Phase 1 진입 대기.<br>납품 필수 2건(<em>위기게이트·교수자 성장 UI</em>)이 미구현이다.</h1>
<p class="pulse-state">코어(저항·반응 조절, 상태머신, RAG, 이론모드, 가드레일, 평가 JSON Schema)는 <b>잘 정렬</b>됐고 회귀도 녹색이다. 막힌 건 두 종류 — <b>내가 실증/결정해야 풀리는 외부 의존</b>(아래 "내 차례")과, <b>착수 대기 중인 납품 필수 기능</b>이다. 이 판은 그 둘을 분리해서 보여준다.</p>
<h1>지금 위치: <em>Phase 0 마감 직전</em> → Phase 1 진입 대기.<br><em>위기게이트 109</em><em>교수자 성장 UI</em>는 1차 구현 완료.</h1>
<p class="pulse-state">코어(저항·반응 조절, 상태머신, RAG, 이론모드, 가드레일, 평가 JSON Schema)는 <b>잘 정렬</b>됐고 회귀도 녹색이다. 막힌 건 두 종류 — <b>내가 실증/결정해야 풀리는 외부 의존</b>(아래 "내 차례")과, <b>남은 원천문서·콘텐츠·운영 갭</b>이다. 이 판은 그 둘을 분리해서 보여준다.</p>
<div class="phase-rail" id="phase-rail" aria-label="Phase 진행 추정"></div>
<div class="mstrip" aria-label="검증 지표">
<span class="mchip good">Full Playwright <b>72</b> 통과</span>
<span class="mchip good">Backend unittest <b>84</b> 통과</span>
<span class="mchip good">Backend pytest <b>118</b> 통과</span>
<span class="mchip good">Layout 시각게이트 <b>7/7</b> accept</span>
<span class="mchip">Pages prod <b>0fe8f3cc</b></span>
<span class="mchip">Pages prod <b>2a58ca49</b></span>
<span class="mchip good">Public API <b>prod OK</b></span>
<span class="mchip crit">원천문서 갭 <b>critical 3</b></span>
<span class="mchip">Decision <b>P0 조건부</b></span>
@ -395,8 +415,8 @@
<g font-family="Pretendard Variable" font-size="13" font-weight="700">
<g><rect x="420" y="40" width="200" height="48" rx="10" fill="#EBF2F0" stroke="#3E7A6E" stroke-width="1.6"/><text x="520" y="69" text-anchor="middle" fill="#2B5249">Phase 0 마감</text></g>
<g><rect x="420" y="144" width="200" height="48" rx="10" fill="#EBF2F0" stroke="#3E7A6E" stroke-width="1.6"/><text x="520" y="173" text-anchor="middle" fill="#2B5249">Phase 1 MVP</text></g>
<g><rect x="420" y="246" width="200" height="48" rx="10" fill="#fff" stroke="#A8352C" stroke-width="1.8"/><text x="520" y="268" text-anchor="middle" fill="#A8352C">위기게이트 109</text><text x="520" y="284" text-anchor="middle" font-size="10" font-weight="600" fill="#A8352C">납품 필수 · 미구현</text></g>
<g><rect x="420" y="336" width="200" height="48" rx="10" fill="#fff" stroke="#A8352C" stroke-width="1.8"/><text x="520" y="358" text-anchor="middle" fill="#A8352C">교수자 성장 UI</text><text x="520" y="374" text-anchor="middle" font-size="10" font-weight="600" fill="#A8352C">납품 필수 · 미구현</text></g>
<g><rect x="420" y="246" width="200" height="48" rx="10" fill="#fff" stroke="#A8352C" stroke-width="1.8"/><text x="520" y="268" text-anchor="middle" fill="#A8352C">위기게이트 109</text><text x="520" y="284" text-anchor="middle" font-size="10" font-weight="600" fill="#A8352C">1차 구현 · 검증</text></g>
<g><rect x="420" y="336" width="200" height="48" rx="10" fill="#fff" stroke="#A8352C" stroke-width="1.8"/><text x="520" y="358" text-anchor="middle" fill="#A8352C">교수자 성장 UI</text><text x="520" y="374" text-anchor="middle" font-size="10" font-weight="600" fill="#A8352C">1차 구현 · 검증</text></g>
<g><rect x="420" y="436" width="200" height="48" rx="10" fill="#EBF2F0" stroke="#3E7A6E" stroke-width="1.6"/><text x="520" y="465" text-anchor="middle" fill="#2B5249">평가 KPI (H1)</text></g>
</g>
<!-- right nodes -->
@ -415,6 +435,84 @@
</div>
</section>
<!-- ░░ DESIGN DIRECTION ░░ -->
<section class="block" id="design-direction">
<div class="sec-head">
<div>
<h2>디자인 시안 지침 — 기능 추출 기반 이미지 생성</h2>
<p class="sub">2026-06-27 기준 주요 페이지 기능을 추출해 PC·태블릿·모바일 반응형 시안 7장과 온화한 이미지 요소 보드 2장을 만들었다. 결과물은 전부 PNG 래스터 이미지이며, 구현 확정안이 아니라 레이아웃·톤·이미지 요소 판단 기준이다.</p>
</div>
<a class="dg-link" href="design-concepts/prompts/vignette-design-prompts-2026-06-27.md">프롬프트 문서</a>
</div>
<div class="dg-shell">
<p class="dg-note">적용 원칙: 생성 이미지 안의 문구는 참고하지 말고 실제 카피는 코드/기획서 기준으로 다시 쓴다. 세션 아바타는 기존 서연/P1 아바타 핸드오프와 `rasterArtSet` 지침을 우선한다. 교수·관리자 내비게이션은 현재 `AppShell`·`Sidebar` 구조를 유지하고, 배경 일러스트·패턴은 빈 상태·히어로·세션 주변 질감에 소량만 쓴다.</p>
<p class="dg-note">1차 적용(2026-06-27): 온화한 이미지 요소 보드는 구현 참고용으로만 두고, 앱 배경에는 전용 래스터 텍스처 `apps/web/public/design-elements/clinical-paper-ambient.png`를 연결했다. 보드·타일셋·누끼 시트 형태 이미지는 직접 UI 배경으로 쓰지 않는다.</p>
<p class="dg-note">2차 적용(2026-06-27): 생성된 관리자/설정/리뷰 시안 기준으로 admin 역할의 상단바·사이드바를 어두운 운영 콘솔 크롬으로 전환했고, 관리자 사용자 관리 행을 조밀한 운영 테이블 밀도로 낮췄다. 회기 리뷰는 헤더·인사이트 카드·피드백 스테이지에 따뜻한 래스터 질감과 낮은 채도 틴트를 적용했다.</p>
<p class="dg-note">3차 적용(2026-06-27): 상담 세션 시작/진행 화면을 시안 기준의 어두운 상담 스테이지로 재정렬했다. 시작 전 화면은 내담자 무대·브리핑·진행 초점 3영역으로 유지하고, 진행 중 화면은 어두운 캔버스 위에 중앙 스테이지와 자막/컨트롤을 배치한다.</p>
<div class="dg-principles" aria-label="디자인 생성 가드레일">
<div><b>래스터만 사용</b><span>이미지 생성 도구 산출물은 PNG 기반 시안이다. SVG·벡터·와이어프레임·로고 시트로 해석하지 않는다.</span></div>
<div><b>기능 우선</b><span>메인 라우트의 실제 액션과 정보 구조를 먼저 반영한다. 장식은 기능을 가리지 않는 수준에서만 쓴다.</span></div>
<div><b>온화하지만 임상적</b><span>종이 질감, 낮은 채도, 차분한 빛은 허용하되 상담 훈련 도구의 신뢰감을 먼저 세운다.</span></div>
<div><b>반응형 세트</b><span>각 핵심 화면은 한 장 안에 PC·태블릿·모바일을 함께 배치해 전환 규칙을 비교한다.</span></div>
</div>
<div class="dg-table-wrap">
<table class="dg-table">
<thead>
<tr><th>화면</th><th>추출한 핵심 기능</th><th>디자인 의도</th></tr>
</thead>
<tbody>
<tr><td>로그인/진입</td><td>Google OAuth, 허용 도메인 안내, 로컬 역할 선택, 교육용 비치료 고지</td><td>신뢰·보안이 먼저 보이는 조용한 진입 화면</td></tr>
<tr><td>학습자 홈</td><td>내담자 선택, 주호소·난도·접근·음성 정보, 새 회기 시작, 기존 회기 이어가기</td><td>오늘 시작할 연습이 명확한 상담 연습 작업대</td></tr>
<tr><td>상담 세션</td><td>회기 단계, 내담자 컨텍스트, 아바타, 음성 오브, 실시간 자막, 텍스트 폴백, 안전 점검</td><td>화면 중심은 내담자, 피드백은 조용한 신호</td></tr>
<tr><td>회기 리뷰</td><td>요약, 감정 밸런스, 회기 흐름, 축어록, 기법 태그, 개선점, 오디오/PDF</td><td>점수 폭격이 아니라 근거 기반 회고</td></tr>
<tr><td>교수 콘솔</td><td>리뷰 대기 큐, 코호트 현황, 페르소나 검수, 종료 회기 목록</td><td>awareness보다 triage에 가까운 교수자 작업대</td></tr>
<tr><td>관리자 콘솔</td><td>서비스 상태, 엔진·DB·음성·리뷰 리소스, 사용자 등록·수정·비활성화</td><td>시스템 건강성과 권한 관리가 한눈에 보이는 운영 화면</td></tr>
<tr><td>설정</td><td>계정, 관리자 AI 운영 설정, 테마, 음성 프리셋, 속도, 알림</td><td>역할별 설정만 보이는 조용한 환경 관리</td></tr>
<tr><td>이미지 요소</td><td>종이 질감, 상담실 책상, 노트, 식물, 부드러운 빛, 패턴, 아바타 분위기</td><td>UI 뒤에서만 느껴지는 따뜻함과 반구상 내담자 톤</td></tr>
</tbody>
</table>
</div>
<div class="dg-gallery" aria-label="생성된 디자인 시안 갤러리">
<article class="dg-card">
<img src="design-concepts/generated/01-login-responsive.png" alt="로그인 화면 PC 태블릿 모바일 반응형 디자인 시안" loading="lazy">
<div class="dg-card-body"><b>로그인/진입</b><span>OAuth, 허용 도메인, 로컬 역할 선택, 교육용 고지를 신뢰 중심으로 정리한 진입 화면.</span><code>01-login-responsive.png</code></div>
</article>
<article class="dg-card">
<img src="design-concepts/generated/02-learner-home-responsive.png" alt="학습자 홈 PC 태블릿 모바일 반응형 디자인 시안" loading="lazy">
<div class="dg-card-body"><b>학습자 홈</b><span>내담자 선택, 회기 시작, 이전 회기 이어가기, 리뷰 가능 상태를 작업대처럼 배치.</span><code>02-learner-home-responsive.png</code></div>
</article>
<article class="dg-card">
<img src="design-concepts/generated/03-session-responsive.png" alt="라이브 상담 세션 PC 태블릿 모바일 반응형 디자인 시안" loading="lazy">
<div class="dg-card-body"><b>라이브 상담 세션</b><span>내담자 무대, 자막, 입력, 라이브 신호, 안전 상태를 몰입형 훈련 화면으로 구성.</span><code>03-session-responsive.png</code></div>
</article>
<article class="dg-card">
<img src="design-concepts/generated/04-session-review-responsive.png" alt="회기 리뷰 PC 태블릿 모바일 반응형 디자인 시안" loading="lazy">
<div class="dg-card-body"><b>회기 리뷰</b><span>감정 흐름, 축어록, 기법 태그, 교수자 콜아웃을 근거 기반 회고 화면으로 묶음.</span><code>04-session-review-responsive.png</code></div>
</article>
<article class="dg-card">
<img src="design-concepts/generated/05-professor-responsive.png" alt="교수 콘솔 PC 태블릿 모바일 반응형 디자인 시안" loading="lazy">
<div class="dg-card-body"><b>교수 콘솔</b><span>리뷰 대기, 코호트 현황, 페르소나 검수, 종료 회기를 triage 중심으로 배치.</span><code>05-professor-responsive.png</code></div>
</article>
<article class="dg-card">
<img src="design-concepts/generated/06-admin-responsive.png" alt="관리자 콘솔 PC 태블릿 모바일 반응형 디자인 시안" loading="lazy">
<div class="dg-card-body"><b>관리자 콘솔</b><span>서비스 상태와 사용자 권한 관리를 운영자가 바로 판단할 수 있는 밀도로 정리.</span><code>06-admin-responsive.png</code></div>
</article>
<article class="dg-card">
<img src="design-concepts/generated/07-settings-responsive.png" alt="설정 화면 PC 태블릿 모바일 반응형 디자인 시안" loading="lazy">
<div class="dg-card-body"><b>설정</b><span>계정, AI 운영, 테마, 음성, 알림을 역할별로 노출하는 차분한 설정 화면.</span><code>07-settings-responsive.png</code></div>
</article>
<article class="dg-card">
<img src="design-concepts/generated/08-warm-visual-elements.png" alt="온화한 배경과 패턴 이미지 요소 보드" loading="lazy">
<div class="dg-card-body"><b>온화한 이미지 요소</b><span>종이 질감, 상담실 책상, 노트, 식물, 낮은 채도의 빛을 UI 보조 요소로 정리.</span><code>08-warm-visual-elements.png</code></div>
</article>
<article class="dg-card">
<img src="design-concepts/generated/09-avatar-pattern-elements.png" alt="반구상 아바타와 패턴 이미지 요소 보드" loading="lazy">
<div class="dg-card-body"><b>아바타/패턴 요소</b><span>반구상 내담자 분위기와 배경 패턴 방향. 실제 페르소나 아이덴티티는 별도 제작 기준을 따른다.</span><code>09-avatar-pattern-elements.png</code></div>
</article>
</div>
</div>
</section>
<!-- ░░ STATUS BOARD ░░ -->
<section class="block" id="board">
<div class="sec-head">
@ -445,21 +543,21 @@
<button class="scard-head" aria-expanded="false"><span class="chip c-done">신규 DONE</span><span class="scard-mid"><span class="scard-title">역린(逆鱗)·트리거 + 수련생 misconduct 반응</span><span class="scard-sum">무례·모욕·조롱 시 현실적 동맹 균열 + 핵심상처 역린 블록 추가. 무회귀 77/77.</span></span><span class="caret" aria-hidden="true"></span></button>
<div class="scard-body"><div class="kv"><b>현재</b><p>라이브 테스트에서 수련생이 내담자를 "수동적 패배자"라 비난·조롱("웃음이 나오십니까")해도 민재가 공손히 흡수 → 훈련 가치 훼손. 수정: ① L0에 무례·모욕·조롱·인신공격 시 <b>현실적 동맹 균열</b>(위축·방어·따지기·종결의향) 지침 ② <b>PersonaCard.triggers(역린)</b> 필드 + CCD 핵심상처 파생 역린 블록(상담자가 핵심상처를 조롱·확정·강요·평가절하 시 가장 강한 반응). PDF '저항·반응 조절' 핵심 차별 강화. 무회귀 77/77.</p></div><div class="kv k-warn"><b>잔여</b><p>P1~P3 명시 triggers는 임상팀 정밀 저작(현재 CCD 파생 기본값으로 작동). 라이브 재현 검증.</p></div></div>
</article>
<article class="scard" data-status="planned" data-cat="안전·위기" data-owner="0">
<button class="scard-head" aria-expanded="false"><span class="chip c-block">CRITICAL</span><span class="scard-mid"><span class="scard-title">위기 안전게이트<span class="critbadge">납품 필수</span></span><span class="scard-sum">자해·자살 감지 시 대화중단+109 안내+지도교수 알림 — 분류·적재만 있고 109·중단·알림 UI 전무.</span></span><span class="caret" aria-hidden="true"></span></button>
<div class="scard-body"><div class="kv"><b>현재</b><p>PDF(9·16·18쪽): 자해·자살 감지 시 <b>대화 중단 + 자살예방상담 109 안내 + 지도교수 자동 알림</b>, 안전은 <b>LLM 밖 별도 CrisisGate</b>. 현 코드: 위기 분류·safety_events 적재만, <b>핫라인 번호가 코드 어디에도 없음</b>(109는 2024.1 1393·1577 통합 — 최신 번호). 대화중단·109안내·교수자 알림 UI 전무.</p></div><div class="kv"><b>검증</b><p>심층조사(2026-06-27, ✓file:line): <code>classify_crisis</code>(guardrail.py:136)·<code>_record_safety_event</code>(sessions.py:694)·<code>app.safety_events</code>(02_schema.sql:221)는 있음. <b>빠진 것</b>: 핫라인 상수(grep 0건)·<code>TurnResponse.crisis_resource</code>·LLM 밖 대화중단 분기·voice.py 경로 safety_event 미적재(REST만 적재).</p></div><div class="kv k-warn"><b>권고</b><p>CrisisGate: LEARNER_REAL 위기 시 턴 중단 + 109/지역 자원 카드 + 교수자 알림(safety_events→대시보드/메일). 답변 생성과 분리. 핫라인 번호·문안·중단정책은 소유자 확정 필요.</p></div></div>
<article class="scard" data-status="done" data-cat="안전·위기" data-owner="0">
<button class="scard-head" aria-expanded="false"><span class="chip c-done">CRITICAL 1차 DONE</span><span class="scard-mid"><span class="scard-title">위기 안전게이트<span class="critbadge">납품 필수</span></span><span class="scard-sum">자해·자살 감지 시 LLM 호출 전 중단, 109 안내, safety_events 적재, 교수자 대시보드 알림 큐까지 연결.</span></span><span class="caret" aria-hidden="true"></span></button>
<div class="scard-body"><div class="kv"><b>현재</b><p>PDF(9·16·18쪽): 자해·자살 감지 시 <b>대화 중단 + 자살예방상담 109 안내 + 지도교수 자동 알림</b>, 안전은 <b>LLM 밖 별도 CrisisGate</b>. 구현: <code>CRISIS_HOTLINE_NUMBER=109</code>, <code>TurnResponse.crisis_resource</code>/<code>conversation_stopped</code>, REST·SSE·voice 경로의 엔진 호출 전 중단, <code>app.safety_events</code> detail 적재, 교수자 대시보드 안전 알림 큐.</p></div><div class="kv"><b>검증</b><p><code>pytest app/test_session_turn_persistence.py app/test_voice_ws.py -q</code> 20 passed, 안전 이벤트 DB insert payload 테스트 포함. <code>pytest app/test_teacher_dashboard.py app/test_session_turn_persistence.py app/test_voice_ws.py -q</code> 21 passed.</p></div><div class="kv k-warn"><b>잔여</b><p>비밀보장 예외고지·생명유지서약 등 임상 문안, 실시간 push/메일 알림, 지역 자원 확장, 운영 정책 문구는 소유자·임상팀 확정 필요. 현재 교수자 알림은 <code>safety_events</code> 기반 대시보드 큐다.</p></div></div>
</article>
<article class="scard" data-status="planned" data-cat="교수자·평가" data-owner="0">
<button class="scard-head" aria-expanded="false"><span class="chip c-block">CRITICAL UX</span><span class="scard-mid"><span class="scard-title">교수자 '학습자 이력·성장 추적' UI/UX<span class="critbadge">핵심 가치</span></span><span class="scard-sum">데이터는 쌓이나 학습자별 성장곡선·항목별 추이 가시화·검수 동선 부재.</span></span><span class="caret" aria-hidden="true"></span></button>
<div class="scard-body"><div class="kv"><b>현재</b><p>PDF 핵심 루프: 학습자가 반복 연습 → <b>교수자가 학습자별 이력·성장곡선(이론부합도·자신감·숙련도, '연습 전·후' 비교)을 검수</b>. 현 교수자 콘솔은 회기 목록 수준 — 학습자별 성장·평가 이력·항목별(공감·질문·정상화·이론부합) 추이를 못 보여줌. 학습 데이터는 쌓이나 <b>가시화·검수 동선 부재</b>.</p></div><div class="kv k-warn"><b>권고</b><p>교수자: 학습자별 대시보드(회기 타임라인·4항목 추이·전후 비교·역린/위기 플래그). 학습자: 성장곡선 + 회기 리뷰 누적.</p></div></div>
<article class="scard" data-status="done" data-cat="교수자·평가" data-owner="0">
<button class="scard-head" aria-expanded="false"><span class="chip c-done">CRITICAL UX 1차 DONE</span><span class="scard-mid"><span class="scard-title">교수자 '학습자 이력·성장 추적' UI/UX<span class="critbadge">핵심 가치</span></span><span class="scard-sum">교수 콘솔에 학습자별 회기 이력, 적절성 추이, 라포, 기법 태그, 최근 회기 포인트를 추가.</span></span><span class="caret" aria-hidden="true"></span></button>
<div class="scard-body"><div class="kv"><b>현재</b><p><code>TeacherDashboardResponse.learner_growth</code>를 추가해 턴별 fast-loop 평가의 <code>appropriateness</code>, <code>rapport_signal</code>, 기법 태그를 학습자별로 집계한다. 교수 콘솔은 학습자별 성장 카드, 회기별 막대 추이, 평균 라포, 변화폭, 최근 회기 점검 포인트를 표시한다. 평가가 없는 회기는 "평가 부족"으로 남겨 가짜 곡선을 만들지 않는다.</p></div><div class="kv"><b>검증</b><p><code>pytest app/test_teacher_dashboard.py app/test_session_turn_persistence.py app/test_voice_ws.py -q</code> 21 passed, <code>pytest app/ -q</code> 119 passed, <code>npm run check:api-types</code>, <code>npm run typecheck</code> passed.</p></div><div class="kv k-warn"><b>잔여</b><p>자기효능감·기술숙련도·수련만족도 사전사후(H1)와 교수자 코멘트/검수 워크플로 확장은 별도 평가설계·임상팀 문항 확정이 필요하다.</p></div></div>
</article>
<article class="scard" data-status="planned" data-cat="거버넌스·아키텍처" data-owner="1">
<button class="scard-head" aria-expanded="false"><span class="chip c-plan">중기</span><span class="scard-mid"><span class="scard-title">2-tier 모델 전략 + 백엔드 언어(원안 차이)<span class="ownbadge">내 차례</span></span><span class="scard-sum">2-tier는 ENGINE_MODE로 점진 도입 가능. 백엔드 언어는 소유자 결정.</span></span><span class="caret" aria-hidden="true"></span></button>
<div class="scard-body"><div class="kv"><b>현재</b><p>① PDF: 고성능(Claude·GPT=내담자발화·실시간평가)/저비용 한국어(Solar=단순응답·사후요약) 2단계 + 캐싱 90%절감. 현재 claude_cli 단일 — 벤더중립 인터페이스는 있음. ② PDF는 <b>Node.js 우선</b>(팀 보유기술·5개월 일정)/Spring 대안. 현재 <b>Python/FastAPI</b> — 재작성은 일정·인력 리스크.</p></div><div class="kv k-warn"><b>권고</b><p>2-tier는 ENGINE_MODE 라우팅으로 점진 도입. 백엔드 언어는 <b>소유자 결정</b>(팀 역량·유지보수 vs 재작성 비용).</p></div></div>
</article>
<article class="scard" data-status="done" data-cat="코어·차별" data-owner="0">
<button class="scard-head" aria-expanded="false"><span class="chip c-done">검증 ✓</span><span class="scard-mid"><span class="scard-title">이번 세션 무회귀 검증 종합</span><span class="scard-sum">백엔드 77~84/84 + E2E 42 + 시각게이트 7/7 통과. 스택 라이브.</span></span><span class="caret" aria-hidden="true"></span></button>
<div class="scard-body"><div class="kv"><b>현재</b><p>페르소나 격리(캐릭터 붕괴 버그)·RAG 비차단(세션생성 블로킹 회귀)·역린/misconduct·init_state 파라미터객체·데드코드(LogHook·tier·RMS) — <b>백엔드 77~84/84 + E2E 42(데스크27·모바일11·아바타4) + 시각게이트 7/7</b> 통과. 스택 라이브(web·api·gateway·voice·RAG).</p></div><div class="kv"><b>다음</b><p>위기 안전게이트(109) → 교수자/학습자 성장 UI 순으로 착수.</p></div></div>
<button class="scard-head" aria-expanded="false"><span class="chip c-done">검증 ✓</span><span class="scard-mid"><span class="scard-title">무회귀 검증 종합</span><span class="scard-sum">백엔드 118/118 + E2E 42 + 시각게이트 7/7 통과. 스택 라이브.</span></span><span class="caret" aria-hidden="true"></span></button>
<div class="scard-body"><div class="kv"><b>현재</b><p>페르소나 격리(캐릭터 붕괴 버그)·RAG 비차단(세션생성 블로킹 회귀)·역린/misconduct·init_state 파라미터객체·데드코드(LogHook·tier·RMS)·OAuth state 복구·X1 dataset export dry-run<b>백엔드 118/118 + E2E 42(데스크27·모바일11·아바타4) + 시각게이트 7/7</b> 통과. 스택 라이브(web·api·gateway·voice·RAG).</p></div><div class="kv"><b>다음</b><p>남은 원천문서 갭과 임상 콘텐츠 적재를 순서대로 닫는다.</p></div></div>
</article>
</div>
</div>
@ -468,45 +566,45 @@
<div class="track-group">
<div class="tg-head"><h3>원천문서 갭 분석</h3><span class="tg-note">산학협력 원천문서 5종 정독 + 코드 교차조사 (2026-06-26). 근거: source-docs-gap-analysis-2026-06-26.md · critical 3 / high 4 / medium+ 6</span></div>
<div class="scards">
<article class="scard" data-status="planned" data-cat="콘텐츠·산출물" data-owner="1">
<button class="scard-head" aria-expanded="false"><span class="chip c-block">CRIT C1</span><span class="scard-mid"><span class="scard-title">사례개념화·치료계획 산출물 구조 전면 부재<span class="critbadge">critical</span><span class="ownbadge">내 차례</span></span><span class="scard-sum">1급 산출물 워크시트가 코드·스키마에 전무(grep 0건). 루브릭은 임상팀 소유.</span></span><span class="caret" aria-hidden="true"></span></button>
<div class="scard-body"><div class="kv"><b>현재</b><p>doc1/4/5가 1급 산출물로 요구하는 워크시트(11탐색항목·호소 5영역·인지삼제·1·2차감정·보호/방해 4사분면·생물심리사회 목표)가 코드·스키마에 전무(grep 0건 ✓직접확인). CCD는 숨은 정답키일 뿐 학습자 산출물 폼이 아님.</p></div><div class="kv k-warn"><b>권고</b><p>워크시트 스키마 입력 폼 + AI 축어록 초안 추출 + 규칙 기반 채점. 루브릭은 임상팀 외부 정의 가능하게 외부화.</p></div></div>
<article class="scard" data-status="done" data-cat="콘텐츠·산출물" data-owner="0">
<button class="scard-head" aria-expanded="false"><span class="chip c-done">C1 1차 DONE</span><span class="scard-mid"><span class="scard-title">사례개념화·치료계획 워크시트 1차 구조<span class="critbadge">critical</span></span><span class="scard-sum">리뷰 API <code>caseWorksheet</code>와 read-only UI 카드로 구조 전무를 해소했다. 임상 루브릭은 외부 의존.</span></span><span class="caret" aria-hidden="true"></span></button>
<div class="scard-body"><div class="kv"><b>현재</b><p>doc1/4/5가 1급 산출물로 요구하는 워크시트 축(탐색 11항목·호소 5영역·인지삼제·1/2차 감정·보호/방해 4사분면·생물심리사회 목표)을 <code>SessionReviewResponse.caseWorksheet</code>로 노출한다. 서버는 저장된 축어록만 근거로 규칙 기반 초안을 만들고, 프론트 리뷰 화면은 근거 턴으로 점프 가능한 read-only 워크시트 카드를 표시한다.</p></div><div class="kv"><b>검증</b><p><code>pytest app/test_session_turn_persistence.py -q</code> 15 passed, <code>pytest app/ -q</code> 119 passed, <code>npm run check:api-types</code>, <code>npm run typecheck</code>, <code>session-review.spec.ts</code> 1 passed.</p></div><div class="kv k-warn"><b>잔여</b><p>편집·DB 저장형 워크시트, 임상팀 루브릭, AI 추출/채점, 교수자 검수 플로우는 후속이다. 현재 초안은 숨은 추론이나 임상 점수 없이 축어록 근거가 있는 항목만 채운다.</p></div></div>
</article>
<article class="scard" data-status="done" data-cat="안전·위기" data-owner="0">
<button class="scard-head" aria-expanded="false"><span class="chip c-done">C2 구조 DONE</span><span class="scard-mid"><span class="scard-title">위기 에스컬레이션·safety_events 배선</span><span class="scard-sum">crisis.risk_level→ideation_observed 전달 + escalate 시 safety_events 적재. 라이브 검증.</span></span><span class="caret" aria-hidden="true"></span></button>
<div class="scard-body"><div class="kv"><b>현재</b><p>prepare_turn→state_machine.evolve에 crisis.risk_level을 ideation_observed로 전달(위기 관측 상태머신 반영). escalate=True 시 <code>app.safety_events</code> 적재(graceful 비차단). <b>라이브</b>: "저 지금 죽고싶어요"→learner_real, safety_events 행 적재 확인. 84/84.</p></div><div class="kv k-warn"><b>잔여(임상팀)</b><p>비밀보장 예외고지·단계적 탐색·생명유지서약 스크립트 콘텐츠 + 실시간 교수자 push(레코드가 1차 알림원) + 위기탐색 누락 회기리뷰 감점.</p></div></div>
<div class="scard-body"><div class="kv"><b>현재</b><p>prepare_turn→state_machine.evolve에 crisis.risk_level을 ideation_observed로 전달(위기 관측 상태머신 반영). escalate=True 시 REST·SSE·voice가 엔진 호출 전 중단하고 <code>app.safety_events</code>에 109 resource와 <code>conversation_stopped</code>를 적재한다. 교수자 대시보드는 이 레코드를 안전 알림 큐로 표시한다. 안전 이벤트 insert payload는 <code>test_record_safety_event_writes_teacher_alert_payload</code>로 회귀화했다.</p></div><div class="kv k-warn"><b>잔여(임상팀)</b><p>비밀보장 예외고지·단계적 탐색·생명유지서약 스크립트 콘텐츠 + 실시간 교수자 push/메일 + 위기탐색 누락 회기리뷰 감점.</p></div></div>
</article>
<article class="scard" data-status="done" data-cat="교수자·평가" data-owner="0">
<button class="scard-head" aria-expanded="false"><span class="chip c-done">C3 구조 DONE</span><span class="scard-mid"><span class="scard-title">이론모드 스레딩(humanistic/CBT)</span><span class="scard-sum">TurnContext+prepare_turn에 theory_mode 배선, 프론트 하드코딩 제거. 84/84.</span></span><span class="caret" aria-hidden="true"></span></button>
<div class="scard-body"><div class="kv"><b>현재</b><p>TurnContext+prepare_turn에 theory_mode, sessions/voice가 sess.theory_mode 전달, evaluator._theory_mode가 세션 이론(학습자 선택) 우선. 프론트 Session.tsx 'humanistic' 하드코딩 제거→persona.theory_target 기준(CBT 페르소나는 cbt). 84/84+typecheck.</p></div><div class="kv k-warn"><b>잔여(임상팀)</b><p>CBT 이론별 단계 프롬프트 체인 콘텐츠 + 이론부합 채점 루브릭 + 프론트 이론 선택 UI(현재 persona 설계 이론 자동).</p></div></div>
<button class="scard-head" aria-expanded="false"><span class="chip c-done">C3 구조 DONE</span><span class="scard-mid"><span class="scard-title">이론모드 스레딩(humanistic/CBT)</span><span class="scard-sum">TurnContext+prepare_turn+build_turn_messages에 theory_mode 배선, 프론트 하드코딩 제거.</span></span><span class="caret" aria-hidden="true"></span></button>
<div class="scard-body"><div class="kv"><b>현재</b><p>TurnContext+prepare_turn에 theory_mode, sessions/voice가 sess.theory_mode 전달, evaluator._theory_mode가 세션 이론(학습자 선택) 우선. <code>build_turn_messages</code><code>THEORY_MODE_GUIDANCE</code>를 통해 인간중심·CBT·통합 이론 프레이밍을 엔진 메시지에 넣는다. 프론트 Session.tsx 'humanistic' 하드코딩 제거→persona.theory_target 기준(CBT 페르소나는 cbt). 회귀: <code>test_prepare_turn_threads_theory_mode_into_engine_messages</code> 포함 focused 20 passed.</p></div><div class="kv k-warn"><b>잔여(임상팀)</b><p>CBT 이론별 단계 프롬프트 체인 콘텐츠 + 이론부합 채점 루브릭 + 프론트 이론 선택 UI(현재 persona 설계 이론 자동).</p></div></div>
</article>
<article class="scard" data-status="planned" data-cat="교수자·평가" data-owner="1">
<button class="scard-head" aria-expanded="false"><span class="chip c-plan">HIGH H1</span><span class="scard-mid"><span class="scard-title">계약 평가 KPI(자기효능감·기술숙련도·수련만족도 사전사후) 수집·집계 전무<span class="ownbadge">내 차례</span></span><span class="scard-sum">doc4가 3척도 pre-post를 명시했으나 코드 0건. 평가설계는 소유자 결정.</span></span><span class="caret" aria-hidden="true"></span></button>
<div class="scard-body"><div class="kv"><b>현재</b><p>doc4가 20명 실험/통제군·단회기 50분·3척도 pre-post를 못박았으나 코드 0건. Phase3 KPI도 report shape만, 계산 코드 0줄. (κ/ICC·환각률은 doc4 미명시 — 평가설계 문서 확정 필요.)</p></div><div class="kv k-warn"><b>권고</b><p>3척도 pre-post 폼·실험/통제군 배정·자동누적 대시보드·추이 시각화·검정 계산 코드. 평가설계는 소유자 결정.</p></div></div>
</article>
<article class="scard" data-status="doing" data-cat="교수자·평가" data-owner="0">
<button class="scard-head" aria-expanded="false"><span class="chip c-doing">H2 부분 DONE</span><span class="scard-mid"><span class="scard-title">턴별 회기 리뷰(상호작용 분석) — 평가 SSOP 적재 시작</span><span class="scard-sum">eval_hook 주입 + fast-loop 평가를 DB 정규화 테이블에 적재·복원. stream 경로는 잔여.</span></span><span class="caret" aria-hidden="true"></span></button>
<div class="scard-body"><div class="kv"><b>현재</b><p>텍스트 <code>/turn</code>·음성 경로에 <code>eval_hook=make_eval_hook(engine_client)</code> 주입. fast-loop 턴 평가는 <code>TurnRecord.evaluation</code>에만 남기지 않고 <code>app.feedback_scores</code>, <code>app.turn_technique</code>, <code>app.turn_client_state</code>, <code>app.supervisor_comment</code>로 정규화 적재 후 리뷰 조회에서 hydrate한다. 원시 평가 테이블은 evaluator/admin RLS로 제한하고 서버가 evaluator 컨텍스트로 적재한다. <code>app.turns</code> insert도 <code>RETURNING id</code> 확인으로 0-row silent success를 차단. 검증: <code>app.test_evaluation_persistence</code> + <code>app.test_session_turn_persistence</code> 14 tests OK.</p></div><div class="kv k-warn"><b>잔여</b><p>stream 경로 fast-loop 평가 미주입 · deep-loop <code>alternative_utterance</code> 정규화 미구현 · safety_events 공통 finalize_turn 미통합 · few-shot 골든셋 기본 OFF · 회기리뷰 2열 UI(축어록 타임라인+비언어) 프론트 강화 · 원천 축어록 골든셋 적재(임상팀).</p></div></div>
<button class="scard-head" aria-expanded="false"><span class="chip c-doing">H2 2차 DONE</span><span class="scard-mid"><span class="scard-title">턴별 회기 리뷰(상호작용 분석) — 평가 SSOP 적재 확대</span><span class="scard-sum">submit/stream/voice 경로의 fast-loop 평가를 DB 정규화 테이블에 적재·복원.</span></span><span class="caret" aria-hidden="true"></span></button>
<div class="scard-body"><div class="kv"><b>현재</b><p>텍스트 <code>/turn</code>·<code>/stream</code>·음성 경로에서 fast-loop 턴 평가를 <code>TurnRecord.evaluation</code>에만 남기지 않고 <code>app.feedback_scores</code>, <code>app.turn_technique</code>, <code>app.turn_client_state</code>, <code>app.supervisor_comment</code>, <code>app.alternative_utterance</code>로 정규화 적재 후 리뷰 조회에서 hydrate한다. 원시 평가 테이블은 evaluator/admin RLS로 제한하고 서버가 evaluator 컨텍스트로 적재한다. <code>app.turns</code> insert도 <code>RETURNING id</code> 확인으로 0-row silent success를 차단. 검증: <code>pytest app/test_evaluation_persistence.py -q</code> 6 passed, <code>pytest app/test_session_turn_persistence.py app/test_evaluation_persistence.py -q</code> 21 passed, <code>pytest app/</code> 119 passed + <code>pytest engine_gateway/</code> 7 passed.</p></div><div class="kv k-warn"><b>잔여</b><p>더 큰 <code>finalize_turn</code>/<code>SessionRepository</code> 경계 · few-shot 골든셋 기본 OFF · 회기리뷰 2열 UI(축어록 타임라인+비언어) 프론트 강화 · 원천 축어록 골든셋 적재(임상팀).</p></div></div>
</article>
<article class="scard" data-status="planned" data-cat="콘텐츠·산출물" data-owner="0">
<button class="scard-head" aria-expanded="false"><span class="chip c-plan">HIGH H3</span><span class="scard-mid"><span class="scard-title">임상팀 콘텐츠 입력 경로(페르소나 저작 CRUD) 부재 + P4~P7 적재</span><span class="scard-sum">검수 승인/반려만 있고 draft 생성·편집 API 없음. SEED만 materialize.</span></span><span class="caret" aria-hidden="true"></span></button>
<div class="scard-body"><div class="kv"><b>현재</b><p>personas 라우트에 검수 승인/반려만, draft 생성·편집 API 없음. persona_repository는 in-code SEED(P1~P3)만 materialize, 외부 JSON 미로드(✓직접확인). doc3 R&R상 콘텐츠는 임상팀 산출물인데 작성 경로가 막힘.</p></div><div class="kv k-warn"><b>권고</b><p>페르소나 저작 CRUD(draft→review) + P4~P7 적재, 루브릭·이론 콘텐츠를 임상팀 편집 가능 데이터로 외부화.</p></div></div>
<article class="scard" data-status="doing" data-cat="콘텐츠·산출물" data-owner="0">
<button class="scard-head" aria-expanded="false"><span class="chip c-doing">HIGH H3 1차 DONE</span><span class="scard-mid"><span class="scard-title">임상팀 콘텐츠 입력 경로(페르소나 저작 CRUD) + P4~P7 적재</span><span class="scard-sum">draft 생성·조회·편집·검수요청 API와 교수 콘솔 JSON 초안 패널 연결. 실제 P4~P7 임상 콘텐츠는 잔여.</span></span><span class="caret" aria-hidden="true"></span></button>
<div class="scard-body"><div class="kv"><b>현재</b><p><code>POST/GET/PUT /personas/drafts</code>를 추가해 teacher/admin이 페르소나 카드를 draft로 생성·조회·편집하고 review 상태로 올릴 수 있다. 교수 콘솔에는 JSON 초안 패널과 검수 큐의 편집 불러오기를 연결했다. 승인된 카드만 학습자 catalog에 노출되는 기존 경계는 유지.</p></div><div class="kv"><b>검증</b><p><code>pytest app/test_persona_review.py -q</code> 19 passed, <code>pytest app/ -q</code> 119 passed, <code>npm run check:api-types</code>, <code>npm run typecheck</code>, <code>npm run build</code> passed.</p></div><div class="kv k-warn"><b>잔여</b><p>P4~P7 실제 임상 콘텐츠 적재, JSON 대신 항목형 저작 UI, 루브릭·이론 콘텐츠 외부화는 후속. 임상팀이 제공하지 않은 페르소나 내용을 임의 생성하지 않는다.</p></div></div>
</article>
<article class="scard" data-status="done" data-cat="보안·PII" data-owner="0">
<button class="scard-head" aria-expanded="false"><span class="chip c-done">H4 부분 DONE</span><span class="scard-mid"><span class="scard-title">한국어 PII 마스킹 보강</span><span class="scard-sum">날짜·금액·행정구역 주소 정규식 추가. 과다마스킹 없음 검증. 6/6+84/84.</span></span><span class="caret" aria-hidden="true"></span></button>
<div class="scard-body"><div class="kv"><b>현재</b><p>정규식 폴백에 한국어 고위험 패턴 추가: 구체 날짜(2001.4.18→[DATE])·금액(1200원→[MONEY])·행정구역 주소(서울시 강남구 역삼동→[ADDR]). 과다마스킹 없음 검증("그냥 힘들어요" 보존), 마스킹·저항 6/6+전체 84/84.</p></div><div class="kv k-warn"><b>잔여</b><p>이름/기관명은 NER 필요(Presidio ko 모델·spaCy ko, 무거움 — TODO 명시) · audit.llm_call_log 런타임 적재 · 미성년/guardian 동의 수집·게이트·철회.</p></div></div>
<button class="scard-head" aria-expanded="false"><span class="chip c-done">H4 부분 DONE</span><span class="scard-mid"><span class="scard-title">PII 마스킹 + LLM 호출 감사</span><span class="scard-sum">날짜·금액·주소 정규식, metadata-only llm_call_log 적재. 전체 API 119.</span></span><span class="caret" aria-hidden="true"></span></button>
<div class="scard-body"><div class="kv"><b>현재</b><p>정규식 폴백에 한국어 고위험 패턴 추가: 구체 날짜(2001.4.18→[DATE])·금액(1200원→[MONEY])·행정구역 주소(서울시 강남구 역삼동→[ADDR]). 상담 생성(generate/stream)·fast/deep 평가의 외부 LLM 호출 직후 <code>audit.llm_call_log</code>에 provider/model/token/cost/inference_geo/latency만 적재한다. prompt/completion 본문은 저장하지 않는다. 검증: 마스킹/감사 7 passed, 관련 회귀 27 passed, 전체 API 119 passed.</p></div><div class="kv k-warn"><b>잔여</b><p>이름/기관명은 NER 필요(Presidio ko 모델·spaCy ko, 무거움 — TODO 명시), 실제 운영 로그인 <code>/turn</code> 후 live DB row proof, 미성년/guardian 동의 수집·게이트·철회.</p></div></div>
</article>
<article class="scard" data-status="planned" data-cat="리팩터·정리" data-owner="0">
<button class="scard-head" aria-expanded="false"><span class="chip c-plan">MED M1~M3</span><span class="scard-mid"><span class="scard-title">비언어 임상이벤트(M1)·다회기 케이스 아크(M2)·SSO claim/식별자/감사(M3)</span><span class="scard-sum">침묵·한숨 이벤트 캡처 전무, case_state 빈 컨텍스트, SSO claim·서명검증 미구현.</span></span><span class="caret" aria-hidden="true"></span></button>
<div class="scard-body"><div class="kv"><b>현재</b><p>M1: voice.py가 EOT용 silence_ms만, 침묵·한숨·울음 타임스탬프 이벤트 캡처·리뷰 표시 전무(서버 RMS 힌트 프론트 dead). M2: case_state·build_recall_context가 빈 컨텍스트 반환(다회기 미구동). M3: cohort_ids=[] 하드코딩·email 파생 식별자·SAML 서명검증 미구현·role변경/삭제 audit 미기록. (분석.)</p></div><div class="kv k-warn"><b>권고</b><p>M1 비언어 메타 이벤트 보존·시각화, M2 case_state 런타임·회상 주입, M3 claim→role/cohort 매핑+안정 식별자+서명검증+deprovisioning audit(기관 의존).</p></div></div>
<button class="scard-head" aria-expanded="false"><span class="chip c-plan">MED M1~M3</span><span class="scard-mid"><span class="scard-title">비언어 임상이벤트(M1)·다회기 케이스 아크(M2)·SSO claim/식별자/감사(M3)</span><span class="scard-sum">M1/M2 1차 완료. M3는 cohort map + provider external_id 1차 완료, 기관 SAML/거버넌스는 남음.</span></span><span class="caret" aria-hidden="true"></span></button>
<div class="scard-body"><div class="kv"><b>현재</b><p>M1: voice learner turn의 audio_ref, silence_ms, speech_rate, barge_in을 리뷰 API <code>nonverbal</code> 이벤트로 파생하고, 회기 리뷰 축어록에 침묵·발화 속도·끼어듦·음성 입력 칩으로 표시한다. 한숨·울음 감지와 live 마이크/STT/TTS 실측은 아직 별도다. M2: <code>case_profile</code>을 learner-persona 안정 case로 upsert하고, <code>session_no</code>를 트랜잭션 안에서 원자 증가시키며, 시작/턴/voice 경로가 직전 <code>session_summary</code> 기반 seed recall을 사용한다. episodic embedding writer와 case_digest/pinned_fact 실적재는 후속. M3: Google/SAML/dev-login이 <code>AUTH_EMAIL_COHORT_MAP</code>/<code>AUTH_DOMAIN_COHORT_MAP</code> 및 SAML cohort claim을 <code>cohort_ids</code>로 전달하고, DB <code>app_user.external_id</code>는 provider subject(<code>google:</code>/<code>saml:</code>/<code>dev:</code>) 기반으로 저장한다.</p></div><div class="kv k-warn"><b>권고</b><p>M1 한숨·울음/억양 등 provider 이벤트 보존, M2 session_summary digest/pinned_fact 압축·embedding writer·case_profile trajectory 갱신, M3 운영 SAML 서명검증·기관 claim schema/test tenant·deprovisioning audit는 외부 IdP 협의 후 진행.</p></div></div>
</article>
<article class="scard" data-status="planned" data-cat="거버넌스·아키텍처" data-owner="1">
<button class="scard-head" aria-expanded="false"><span class="chip c-plan">TRACK X1·X2·L1</span><span class="scard-mid"><span class="scard-title">재귀학습 export(X1)·API 비용 관측(X2)·스택 정합성·일정(L1)<span class="ownbadge">내 차례</span></span><span class="scard-sum">export·IAA 게이트 미코딩, 토큰 텔레메트리 0 고정, 신청서 스택 불일치.</span></span><span class="caret" aria-hidden="true"></span></button>
<div class="scard-body"><div class="kv"><b>현재</b><p>X1: ds.* 스키마만, read/write·JSONL export·IAA 게이트 미코딩. X2: 게이트웨이 토큰 텔레메트리 0 고정·cost 모니터링 부재(doc3가 비용을 운영 리스크로 명시). L1: doc4 신청서 스택(Spring Boot 3/Node.js·TimescaleDB) vs 실제 FastAPI/Python 불일치, 20주 단기일정·9월 저작권 등재 압박.</p></div><div class="kv k-warn"><b>권고</b><p>X1 export 잡·IAA 게이트(κ≥0.6/ICC≥0.75), X2 토큰 텔레메트리·캐싱·비용 대시보드, L1 스택 정합 또는 변경사유 거버넌스 기록.</p></div></div>
<button class="scard-head" aria-expanded="false"><span class="chip c-plan">TRACK X1·X2 1차·L1</span><span class="scard-mid"><span class="scard-title">재귀학습 export(X1)·API 비용 관측(X2)·스택 정합성·일정(L1)<span class="ownbadge">내 차례</span></span><span class="scard-sum">X1/X2 1차는 구현 완료. X1 approved export와 L1 거버넌스는 외부 gate가 남음.</span></span><span class="caret" aria-hidden="true"></span></button>
<div class="scard-body"><div class="kv"><b>현재</b><p>X1: <code>scripts/export-recursive-dataset.py</code><code>app.services.dataset_export</code>로 masked-text JSONL dry-run, PII scan, κ/ICC 계산, approved export 게이트를 구현했다. 기본은 <code>technical_dry_run</code>이고 <code>ds.*</code> write는 <code>--write-dataset</code> 명시 시에만 수행한다. X2: <code>app.turns</code>의 provider/model/tokens/cost를 최근 7일 기준으로 집계하는 <code>GET /admin/usage</code>를 추가했고, 운영 콘솔 <code>/admin</code>에 누적 비용·입출력 토큰·계량 커버리지·provider/model breakdown과 <code>ADMIN_USAGE_BUDGET_USD</code> 기반 예산 경고(ok/warn/exceeded)를 표시한다. DB 미가용 dev에서는 runtime store로 fallback하지만 prod에서는 fail-closed한다. L1: doc4 신청서 스택(Spring Boot 3/Node.js·TimescaleDB) vs 실제 FastAPI/Python 불일치, 20주 단기일정·9월 저작권 등재 압박.</p></div><div class="kv k-warn"><b>권고</b><p>X1 approved export·골든셋 승격은 steward/legal 승인, reviewer disposition, IAA 게이트(κ≥0.6/ICC≥0.75) 이후만 진행한다. X2 후속은 캐싱·저비용 모델 분기, L1은 스택 정합 또는 변경사유 거버넌스 기록.</p></div></div>
</article>
</div>
<div class="detail-blocks">
<div class="detail-block"><h4>즉시 착수 가능(내부 코드, 외부 합의 불요)</h4><ul><li>C2 배선: escalate 시 safety_events insert + ideation_observed 전달.</li><li>C3 골격: theory 인자 + Session.tsx 하드코딩 제거 + 프론트 선택 UI.</li><li>H2 배선: eval_hook을 turn 파이프라인 주입(이미 정의·export).</li><li>H3: 페르소나 저작 CRUD + P4~P7 로드.</li><li>H4(부분)·M1(부분)·M2·X1·X2: 마스킹·메타 이벤트·회상·export·텔레메트리.</li></ul></div>
<div class="detail-block"><h4>즉시 착수 가능(내부 코드, 외부 합의 불요)</h4><ul><li>C1/C2/C3/H2/H3은 1차 구조 완료. 후속은 저장형 워크시트, 임상 스크립트, CBT 체인, 2열 리뷰 UI, P4~P7 콘텐츠 적재처럼 데이터·정책·UI 고도화 영역이다.</li><li>H4(부분): 한국어 정규식 마스킹과 metadata-only LLM call audit 경로는 완료. 남은 내부 후보는 이름/기관 NER와 동의 게이트 골격이고, live row proof는 운영 로그인 증거가 필요하다. X1 dry-run export 1차와 X2 비용 관측·예산 경고 1차는 완료, X2 캐싱은 후속. M1/M2/M3 1차는 완료, provider 기반 한숨·울음 감지와 case_digest/pinned_fact 실적재, 운영 SAML 서명검증은 후속.</li></ul></div>
<div class="detail-block"><h4>소유자 결정 / 외부(임상팀·기관) 의존</h4><ul><li>임상팀 산출물: C1 워크시트 항목·루브릭, C3 CBT 콘텐츠·체인, C2 위기 스크립트·서약 문안, H2 골든셋 코딩.</li><li>소유자 평가설계: H1 실험/통제군·3척도 문항·50분 흐름, κ/ICC·환각률 목표(doc4 미명시).</li><li>기관(한신 IT): M3 SSO IdP·test tenant·claim 스키마·인증서, 거버넌스 증거.</li><li>거버넌스: L1 스택 정합성 방향·저작권 등재 문서화·IP 협의.</li></ul></div>
<div class="detail-block"><h4>판독 한계 (정직성)</h4><ul><li>핵심 축어록 2단 표는 PDF 시각 판독이 1차 근거(txt 일부 누락), OCR 잔재 존재.</li><li>doc4 κ/ICC·환각률은 신청서 본문 미명시 — 계약 확정 지표로 단정 금지.</li><li>'현재상태'는 grep/코드 사실 기반. C1~C3·H2·H3은 직접 재확인(✓), 그 외는 착수 전 코드 1차 재확인 권고.</li></ul></div>
</div>
@ -517,8 +615,8 @@
<div class="tg-head"><h3>코드품질 · 로컬 개발 스택</h3><span class="tg-note">리팩토링 연구 + 1-커맨드 dev 스택 (2026-06-26~27). code-quality-research-2026-06-26.md · local-development.md</span></div>
<div class="scards">
<article class="scard" data-status="done" data-cat="인프라·스택·RAG" data-owner="0">
<button class="scard-head" aria-expanded="false"><span class="chip c-done">DEV STACK</span><span class="scard-mid"><span class="scard-title">로컬 개발 스택 1-커맨드 기동 — <code>scripts/dev-up.ps1</code></span><span class="scard-sum">게이트웨이9099+API8000+웹5173 결정론적 기동. 3포트 health OK + 실 AI 턴.</span></span><span class="caret" aria-hidden="true"></span></button>
<div class="scard-body"><div class="kv"><b>현재</b><p>게이트웨이(9099 claude_cli)+API(8000, DB 미가용 시 in-memory degraded)+웹(5173)을 커맨드라인 기준 정리 후 결정론적 기동(<code>--reload</code> 워처 불안정 회피, uvicorn 있는 python 자동 해석). dev-login + seed 페르소나. 로그 <code>.devlogs/</code>(gitignore). <b>검증</b>: 3포트 health OK + 실제 AI 턴 생성·회기리뷰 동작.</p></div><div class="kv k-warn"><b>비고</b><p>AI 턴은 로컬 claude CLI 사용(사용량 발생). 진입점 http://localhost:5173. <b>주의(무DB 한정)</b>: DB 없이 degraded면 페르소나가 <code>source:seed_fallback·degraded:true</code>라 프론트 <code>isUsablePersona</code>가 막아 "회기 시작" 버튼이 비활성 → UI 세션 시작엔 실제 Postgres(<code>127.0.0.1:55432</code>)가 필요.</p></div></div>
<button class="scard-head" aria-expanded="false"><span class="chip c-done">DEV STACK</span><span class="scard-mid"><span class="scard-title">로컬 개발 스택 1-커맨드 기동 — <code>scripts/dev-up.ps1</code></span><span class="scard-sum">게이트웨이9099+API8000+웹5173+Docker DB 보장. 3포트 health OK + 실 AI 턴.</span></span><span class="caret" aria-hidden="true"></span></button>
<div class="scard-body"><div class="kv"><b>현재</b><p>게이트웨이(9099 claude_cli)+API(8000, DB 미가용 시 in-memory degraded)+웹(5173)을 커맨드라인 기준 정리 후 결정론적 기동(<code>--reload</code> 워처 불안정 회피, uvicorn 있는 python 자동 해석). Docker가 사용 가능하면 <code>vignette-dev-db</code> healthcheck/pg_isready/role safety를 점검하고, compose <code>infra\.env</code> 필수값 누락도 경고한다. dev-login + seed 페르소나. 로그 <code>.devlogs/</code>(gitignore). <b>검증</b>: 3포트 health OK + 실제 AI 턴 생성·회기리뷰 동작.</p></div><div class="kv k-warn"><b>비고</b><p>AI 턴은 로컬 claude CLI 사용(사용량 발생). 진입점 http://localhost:5173. <code>dev-down.ps1</code>은 기본적으로 DB 컨테이너를 보존하고, <code>-Db</code> 지정 시 함께 중지한다. 기존 <code>vignette-dev-db</code>는 accepting connections이나 healthcheck가 없고, <code>vignette_app</code>은 NOBYPASSRLS로 존재하지만 현재 API startup DDL은 owner 권한이 필요해 런타임 app-role 분리는 후속이다. <b>주의(무DB 한정)</b>: DB 없이 degraded면 페르소나가 <code>source:seed_fallback·degraded:true</code>라 프론트 <code>isUsablePersona</code>가 막아 "회기 시작" 버튼이 비활성 → UI 세션 시작엔 실제 Postgres(<code>127.0.0.1:55432</code>)가 필요.</p></div></div>
</article>
<article class="scard" data-status="done" data-cat="인프라·스택·RAG" data-owner="0">
<button class="scard-head" aria-expanded="false"><span class="chip c-done">VOICE 작동</span><span class="scard-mid"><span class="scard-title">음성 채팅 "바로 꺼짐" 해결 — provider 키 부재였음</span><span class="scard-sum">키 없으면 1011 즉시 close. .env에 OPENAI_API_KEY 설정→정상. TTS 지문 제거·아바타 모션 수정.</span></span><span class="caret" aria-hidden="true"></span></button>
@ -526,18 +624,18 @@
</article>
<article class="scard" data-status="done" data-cat="코어·차별" data-owner="0">
<button class="scard-head" aria-expanded="false"><span class="chip c-done">페르소나 격리</span><span class="scard-mid"><span class="scard-title">AI 내담자 캐릭터 붕괴·개발맥락 누출 버그 수정</span><span class="scard-sum">--append→--system-prompt 교체 + dynamic 섹션 제외. 누출 0. 게이트웨이 7/7.</span></span><span class="caret" aria-hidden="true"></span></button>
<div class="scard-body"><div class="kv"><b>증상</b><p>라이브 회기에서 내담자(민재)가 "저는 Claude Code인데… Vignette 코드베이스… git status…"로 캐릭터를 깨고 개발 맥락 노출. 원인: ENGINE_MODE=claude_cli가 로컬 claude(=Claude Code)를 호출하는데 게이트웨이가 페르소나를 <code>--append-system-prompt</code>로 코딩 어시스턴트 정체성에 <b>덧붙이고</b>, cwd=repo라 CLAUDE.md·git status·env가 시스템프롬프트에 포함됨.</p></div><div class="kv"><b>수정·검증</b><p><code>engine_gateway/gateway.py</code>: <code>--append-system-prompt</code><code>--system-prompt</code>(페르소나만 <b>교체</b> 주입) + <code>--exclude-dynamic-system-prompt-sections</code>(cwd/env/git/메모리 섹션 제거). 라이브: 깨던 입력 그대로 → 민재 완벽 인캐릭터, 누출 0. 게이트웨이 7/7 + 전체 84/84.</p></div></div>
<div class="scard-body"><div class="kv"><b>증상</b><p>라이브 회기에서 내담자(민재)가 "저는 Claude Code인데… Vignette 코드베이스… git status…"로 캐릭터를 깨고 개발 맥락 노출. 원인: ENGINE_MODE=claude_cli가 로컬 claude(=Claude Code)를 호출하는데 게이트웨이가 페르소나를 <code>--append-system-prompt</code>로 코딩 어시스턴트 정체성에 <b>덧붙이고</b>, cwd=repo라 CLAUDE.md·git status·env가 시스템프롬프트에 포함됨.</p></div><div class="kv"><b>수정·검증</b><p><code>engine_gateway/gateway.py</code>: <code>--append-system-prompt</code><code>--system-prompt</code>(페르소나만 <b>교체</b> 주입) + <code>--exclude-dynamic-system-prompt-sections</code>(cwd/env/git/메모리 섹션 제거). 라이브: 깨던 입력 그대로 → 민재 완벽 인캐릭터, 누출 0. 게이트웨이 7/7 + 전체 API 119 passed.</p></div></div>
</article>
<article class="scard" data-status="doing" data-cat="리팩터·정리" data-owner="0">
<button class="scard-head" aria-expanded="false"><span class="chip c-doing">REFACTOR 1</span><span class="scard-mid"><span class="scard-title">턴 처리 파이프라인 중복(REST/WS) 제거</span><span class="scard-sum">submit/stream/voice가 턴 처리 복제. 공통 record_completed_turn 추출.</span></span><span class="caret" aria-hidden="true"></span></button>
<div class="scard-body"><div class="kv"><b>현재</b><p><code>routes/sessions.py</code>(submit/stream)·<code>routes/voice.py</code>가 세션로드→오너십→prepare_turn→run_turn_generate→TurnRecord append→state update를 복제. 일부 정리: sync/voice eval_hook은 주입됐고 fast-loop 평가는 DB 정규화 적재를 시작. 잔여 실드리프트: stream 평가 누락, voice 빈 RecallContext, safety_events 저장 경로 분산. 공통 <code>finalize_turn</code>/<code>SessionRepository</code>로 추출.</p></div><div class="kv k-warn"><b>안전</b><p>순수 리팩터 단독 PR + 골든 테스트(test_session_turn_persistence·test_voice_ws) 동등성 확인.</p></div></div>
<button class="scard-head" aria-expanded="false"><span class="chip c-doing">REFACTOR 1 1차 DONE</span><span class="scard-mid"><span class="scard-title">턴 처리 파이프라인 중복(REST/WS) 제거</span><span class="scard-sum"><code>turn_runtime.py</code>로 세션 로드·권한·턴 append·상태 갱신 공통화.</span></span><span class="caret" aria-hidden="true"></span></button>
<div class="scard-body"><div class="kv"><b>현재</b><p><code>app/turn_runtime.py</code>를 추가해 <code>load_owned_session</code>, <code>record_completed_turn</code>, <code>append_completed_turn</code>, <code>update_session_state</code>, <code>record_safety_event</code>를 공통화했다. <code>routes/sessions.py</code>의 submit/stream과 <code>routes/voice.py</code>가 같은 저장·위기 이벤트 경로를 사용하고, stream fast-loop 평가 누락도 닫았다. voice 경로도 세션 recall cache를 사용한다. 검증: 관련 회귀 45 passed, 전체 API 119 passed, gateway 7 passed.</p></div><div class="kv k-warn"><b>잔여</b><p>더 큰 <code>finalize_turn</code>/<code>SessionRepository</code> 경계, 장기적으로는 세션 저장소 인터페이스 SSOT화가 남아 있다.</p></div></div>
</article>
<article class="scard" data-status="planned" data-cat="리팩터·정리" data-owner="0">
<button class="scard-head" aria-expanded="false"><span class="chip c-plan">REFACTOR 2</span><span class="scard-mid"><span class="scard-title">백엔드 Pydantic ↔ 프론트 <code>lib/api.ts</code> 계약 SSOT화</span><span class="scard-sum">~14개 응답 모델 수기 미러(드리프트 실증). OpenAPI 단방향 생성으로 전환.</span></span><span class="caret" aria-hidden="true"></span></button>
<div class="scard-body"><div class="kv"><b>현재</b><p>~14개 응답 모델이 TS에 수기 미러(드리프트 실증: <code>crisis_kind</code>·<code>end_state</code> 누락). OpenAPI→openapi-typescript 단방향 생성으로 전환(타입만, 저위험).</p></div><div class="kv k-warn"><b>안전</b><p>CI에 "재생성 후 git diff 없음" 게이트.</p></div></div>
<article class="scard" data-status="doing" data-cat="리팩터·정리" data-owner="0">
<button class="scard-head" aria-expanded="false"><span class="chip c-doing">REFACTOR 2 1차 DONE</span><span class="scard-mid"><span class="scard-title">백엔드 Pydantic ↔ 프론트 <code>lib/api.ts</code> 계약 SSOT화</span><span class="scard-sum">OpenAPI→<code>api.gen.ts</code> 생성 + stale 체크 게이트. <code>crisis_kind</code>/<code>end_state</code> 드리프트 해소.</span></span><span class="caret" aria-hidden="true"></span></button>
<div class="scard-body"><div class="kv"><b>현재</b><p><code>apps/api/scripts/export-openapi.py</code><code>apps/web/scripts/generate-api-types.mjs</code>로 FastAPI OpenAPI를 <code>apps/web/src/lib/api.gen.ts</code>에 생성한다. <code>npm run check:api-types</code>가 재생성 누락을 잡고, <code>TurnResponse</code>/<code>SessionEndResponse</code>는 생성 타입 alias를 사용한다. 기존 누락 필드 <code>crisis_kind</code>, <code>end_state</code>, <code>crisis_resource</code>, <code>safety_alerts</code>, <code>learner_growth</code>, <code>PersonaDraftPayload</code>는 생성 타입에 포함된다. 검증: <code>npm run check:api-types</code>, <code>npm run typecheck</code>, <code>pytest app/</code> 119 passed, <code>pytest engine_gateway/</code> 7 passed.</p></div><div class="kv k-warn"><b>잔여</b><p>나머지 수기 미러 타입(<code>PersonaSummary</code>, review/admin/teacher/user DTO 등)을 생성 타입 alias로 점진 전환하고, 실제 CI 워크플로에 <code>npm run check:api-types</code>를 연결해야 한다.</p></div></div>
</article>
<article class="scard" data-status="done" data-cat="리팩터·정리" data-owner="0">
<button class="scard-head" aria-expanded="false"><span class="chip c-done">REFACTOR 3 일부</span><span class="scard-mid"><span class="scard-title">죽은 확장점/계약 정리 — LogHook·tier 제거 적용</span><span class="scard-sum">caller 0 타입·분기·export 제거. 잔여는 패치 문서에 스테이징. 84/84.</span></span><span class="caret" aria-hidden="true"></span></button>
<button class="scard-head" aria-expanded="false"><span class="chip c-done">REFACTOR 3 일부</span><span class="scard-mid"><span class="scard-title">죽은 확장점/계약 정리 — LogHook·tier 제거 적용</span><span class="scard-sum">caller 0 타입·분기·export 제거. 잔여는 패치 문서에 스테이징. 전체 API 119.</span></span><span class="caret" aria-hidden="true"></span></button>
<div class="scard-body"><div class="kv"><b>현재</b><p><code>LogHook</code>/<code>log_hook</code>(caller 0) 타입·파라미터·분기·export 제거 · 게이트웨이/엔진클라 <code>tier</code> dead-contract 제거(write-only, gateway는 model만 사용). 잔여(RMS 힌트·P4~P7·Live2D 고아 자산·파라미터객체 init_state/prepare_turn/upsert)는 <code>docs/ops/refactor-rag-patches-2026-06-26.{md,json}</code>에 스테이징(단계 적용).</p></div><div class="kv"><b>참고</b><p>일괄 적용 대신 라이브 스택 보호 위해 그룹별 적용+pytest 검증.</p></div></div>
</article>
<article class="scard" data-status="done" data-cat="인프라·스택·RAG" data-owner="0">
@ -590,15 +688,15 @@
<div class="task-row"><div><span class="task-status s-risk">BLOCK</span></div><div><b>공개 Google OAuth 실제 <code>/turn</code> proof 확보</b><p>로그인 가능한 계정으로 storageState를 저장한 뒤 public-auth-turn E2E를 실행한다.</p></div><div><b>산출물</b><p><code>apps/web/node_modules/.tmp/public-auth.json</code>, public <code>/turn</code> 성공 로그</p></div><div><b>검증</b><p><code>E2E_PUBLIC_AUTH=1</code> + <code>chromium-public-auth</code> 1회 통과</p></div></div>
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>engine config 운영값 재확인</b><p><code>GET /admin/engine-config</code>로 운영값을 직접 확인했다. <code>engine_mode=claude_cli</code>, <code>engine_url=http://127.0.0.1:9099</code>, <code>model=gateway-default</code>, <code>durable=true</code>, <code>source=database</code>로 의도값과 일치한다.</p></div><div><b>산출물</b><p>config API 응답(durable, db source)</p></div><div><b>검증</b><p><code>claude_cli / http://127.0.0.1:9099 / gateway-default</code> 의도값 대조 완료</p></div></div>
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>과거 Live2D demo 자산 제거</b><p>Mao/Haru 샘플, Pixi/Cubism 런타임, 공개 <code>/live2d/*</code> 캐시 잔여 접근을 차단했다.</p></div><div><b>산출물</b><p><code>apps/web/functions/live2d/[[path]].js</code>, P1 PSB raster avatar + SVG fallback</p></div><div><b>검증</b><p>운영 <code>/live2d/mao/*</code>, Cubism core <code>404</code></p></div></div>
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>프로세스 난립 정리</b><p>임시 Vite preview와 dev API를 내리고 운영 확인용 프로세스만 유지한다.</p></div><div><b>산출물</b><p>9099 engine, 8001 prod API, cloudflared tunnel 1개</p></div><div><b>검증</b><p><code>Get-NetTCPConnection</code>에서 8000/517x/5537 비어 있음</p></div></div>
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>프로세스 난립 정리</b><p>운영 확인용 프로세스와 로컬/Tailnet 검증용 dev 프로세스를 의도적으로 분리해 유지한다.</p></div><div><b>산출물</b><p>9099 engine, 8001 prod API, 8000/8010 dev API, 5173 web, cloudflared tunnel 1개</p></div><div><b>검증</b><p><code>Get-NetTCPConnection</code>에서 대상 포트별 listener 확인</p></div></div>
<div class="task-row"><div><span class="task-status s-plan">GATE</span></div><div><b>한신대 공문/데이터 거버넌스 게이트 정리</b><p>SSO 클레임, 추가 축어록 수급, 미성년 원본 활용동의, 개인정보 처리방침을 P1 진입 전 외부 의존성으로 명확히 둔다. 로컬 문서 골격은 준비됐지만 한신대/데이터 steward의 written evidence는 아직 필요하다.</p></div><div><b>산출물</b><p><code>docs/ops/hanshin-data-governance-gate.md</code>, 공문 질의 항목, 동의 범위 체크리스트, SSO claim mapping 표</p></div><div><b>검증</b><p>문서 artifact 작성 완료; IRB 게이트가 아니라 데이터/SSO 게이트로 외부 증거 필요 상태 유지</p></div></div>
</div>
<div class="source-note"><b>팀장 판정:</b> 로컬 회귀와 공개 prod-safe 게이트는 통과했다. Cloudflare Pages production <code>0fe8f3cc</code><code>543d48c</code> 소스로 배포됐고, 운영 반영 전 남은 핵심 증거는 공개 Google OAuth 실제 <code>/turn</code> smoke다. 런타임 mock/demo 자산은 운영 번들에서 제거하고 테스트 fixture만 남긴다.</div>
<div class="source-note"><b>팀장 판정:</b> 로컬 회귀와 공개 prod-safe 게이트는 통과했다. Cloudflare Pages production <code>2a58ca49</code>이 현재 워킹트리 dist로 배포됐고, 운영 반영 전 남은 핵심 증거는 공개 Google OAuth 실제 <code>/turn</code> smoke다. 런타임 mock/demo 자산은 운영 번들에서 제거하고 테스트 fixture만 남긴다.</div>
<div class="tab-subhead"><h4>Phase 0 완료 상세 목록</h4><p>이전 "완료" 카드 내용을 탭 안에서 항목별로 보존한다.</p></div>
<div class="task-list">
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>DB persona 기반 테스트</b><p><code>session-persistence</code>, <code>teacher</code>, <code>voice</code><code>/api/personas</code>에서 실제 DB persona를 가져온다.</p></div><div><b>산출물</b><p>seed persona 직접 결합 제거, 실제 persona catalog 기반 E2E</p></div><div><b>검증</b><p>persona 관련 E2E가 DB-backed persona로 통과</p></div></div>
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>비개발 런타임 보호</b><p>prod/staging에서 dev login, seed fallback, OAuth 누락, 기본 secret, localhost CORS를 거부한다.</p></div><div><b>산출물</b><p>fail-closed runtime config validation</p></div><div><b>검증</b><p>backend runtime policy unittest 통과</p></div></div>
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>compose OAuth 주입</b><p><code>ENVIRONMENT</code>, Google OAuth vars, <code>CORS_ORIGINS</code>, <code>OPENAI_BASE_URL</code>을 API 컨테이너에 명시했다.</p></div><div><b>산출물</b><p>compose 환경 주입 정리</p></div><div><b>검증</b><p><code>docker compose --env-file .env.example config --quiet</code> 통과</p></div></div>
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>compose OAuth 주입</b><p><code>ENVIRONMENT</code>, Google OAuth vars, <code>CORS_ORIGINS</code>, <code>OPENAI_BASE_URL</code>을 API 컨테이너에 명시했다.</p></div><div><b>산출물</b><p>compose 환경 주입 정리</p></div><div><b>검증</b><p><code>docker compose -f infra\docker-compose.yml --env-file infra\.env.example config --quiet</code> 더미 필수 env 세트로 통과</p></div></div>
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>admin 모바일 overflow 수정</b><p>긴 readiness detail을 안정화했고, 사용자 변경은 DB 저장소가 durable할 때만 허용한다.</p></div><div><b>산출물</b><p>admin responsive layout, durable store gate, AI health copy</p></div><div><b>검증</b><p>admin manage-users desktop/mobile 2 passed</p></div></div>
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>global engine config 테스트 분리</b><p><code>db-persistence</code>는 전역 engine config를 바꾸므로 <code>@single-run</code>으로 분리했다.</p></div><div><b>산출물</b><p>Playwright single-run tagging</p></div><div><b>검증</b><p>전체 Playwright 72 passed</p></div></div>
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>handoff와 문서 갱신</b><p><code>docs/HANDOFF.md</code>, <code>docs/dev_dashboard.html</code>, <code>apps/web/e2e/README.md</code>를 최신 상태로 맞췄다.</p></div><div><b>산출물</b><p>handoff, dashboard, E2E README</p></div><div><b>검증</b><p>최신 커밋 기준 상태/검증/운영 포트 일치</p></div></div>
@ -621,7 +719,7 @@
<div class="task-list">
<div class="task-row"><div><span class="task-status s-doing">CHECK</span></div><div><b>LLMEngine 정규화 계약 고정</b><p>gateway SSE <code>token/done/error</code>와 generate cost 계약은 회귀화했다. claude_cli와 Messages API 폴백 동일성 검증은 남아 있다.</p></div><div><b>산출물</b><p><code>token</code>, <code>done</code>, <code>error</code>, cost 이벤트 계약과 contract test</p></div><div><b>검증</b><p>stream error 미저장 + stream/generate client turn telemetry 저장</p></div></div>
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>상주 <code>claude -p</code> 엔진풀 실증</b><p><code>session_id</code>가 살아 있는 gateway session을 재사용하고 missing session은 ephemeral로 닫히는 구조를 회귀화했고, live 게이트웨이(9099)로 실측까지 마쳤다. <code>probe-engine-gateway.py</code>가 단일 session_id로 2회 reused stream을 돌려 TTFT 16674199ms, cost 누적 0.068→0.123(turns 1→2), engine=claude_p, model=opus-4-8을 측정했다.</p></div><div><b>산출물</b><p><code>engine_gateway/test_gateway_model.py</code>, <code>scripts/probe-engine-gateway.py</code>, live probe JSON</p></div><div><b>검증</b><p>session reuse 7 tests OK; live probe 2 reused streams, TTFT/cost 실측, session_id 재사용 확인</p></div></div>
<div class="task-row"><div><span class="task-status s-doing">CHECK</span></div><div><b>마스킹 게이트 강제</b><p>current turn, recall, pinned fact, recent turns가 Presidio/가명처리 후에만 engine request로 들어가게 막았다. 운영 외부-send 관측 증거는 남아 있다.</p></div><div><b>산출물</b><p><code>app/test_orchestrator_masking.py</code>, context 입력 재마스킹</p></div><div><b>검증</b><p>phone/email/RRN raw 값이 generate/stream payload에 없음</p></div></div>
<div class="task-row"><div><span class="task-status s-doing">CHECK</span></div><div><b>마스킹 게이트 + LLM call audit</b><p>current turn, recall, pinned fact, recent turns가 Presidio/가명처리 후에만 engine request로 들어가게 막았다. 외부 LLM 호출은 <code>audit.llm_call_log</code>에 provider/model/token/cost/inference_geo/latency metadata만 남기고 prompt/completion 본문은 저장하지 않는다. 실제 운영 로그인 <code>/turn</code> 후 live DB row proof는 남아 있다.</p></div><div><b>산출물</b><p><code>app/test_orchestrator_masking.py</code>, <code>app/test_evaluation_persistence.py</code>, <code>session_persistence.record_llm_call_audit</code></p></div><div><b>검증</b><p>phone/email/RRN raw 값이 generate/stream payload에 없음, 감사 payload 본문 미포함, 관련 회귀 27 passed, 전체 API 119 passed</p></div></div>
<div class="task-row"><div><span class="task-status s-doing">CHECK</span></div><div><b>P1 상담 완주 플로우</b><p>통제된 UI E2E로 P1 선택, SSE 상담, 종료, 회기말 리뷰 피드백을 한 번에 통과시켰다. 실제 운영 로그인 proof는 P0 blocker와 함께 남아 있다.</p></div><div><b>산출물</b><p><code>e2e/session-mvp.spec.ts</code>, turn telemetry 저장 경로</p></div><div><b>검증</b><p><code>chromium-single-run</code> P1 MVP UI E2E 1 passed</p></div></div>
<div class="task-row"><div><span class="task-status s-doing">CHECK</span></div><div><b>저항 엔진 정량 검증</b><p>공감 발화와 조언점프 발화 5턴 세트를 순수 상태머신 회귀 테스트로 비교했다. DB 쿼리 기반 운영 증거는 남아 있다.</p></div><div><b>산출물</b><p><code>app/test_state_machine_resistance.py</code></p></div><div><b>검증</b><p>공감 시 탐색 전이/개방도 상승, 조언점프 시 라포 단계 유지/개방도 0</p></div></div>
<div class="task-row"><div><span class="task-status s-doing">CHECK</span></div><div><b>음성 왕복 1턴 PoC</b><p>Session UI 마이크 버튼 경로에서 synthetic <code>getUserMedia</code>/<code>MediaRecorder</code>로 실제 <code>/voice/ws</code>를 열고 audio_start, binary chunk, audio_end, transcript, reply, TTS 재생 상태까지 검증했다. 물리 마이크, 공개 WSS, live STT/TTS provider 증거는 아직 별도다.</p></div><div><b>산출물</b><p><code>apps/web/e2e/voice-success.spec.ts</code>, controlled OpenAI/engine providers, browser voice UI probe</p></div><div><b>검증</b><p><code>npx playwright test e2e/voice-success.spec.ts --project=chromium-single-run</code> 2 passed</p></div></div>
@ -631,7 +729,7 @@
<div class="tab-panel" id="panel-p2a" role="tabpanel" aria-labelledby="tab-p2a" hidden>
<div class="detail-head"><div><h3>Phase 2a 권한, OAuth, 페르소나 상세</h3><p>음성과 섞지 않고 3역할, 권한격리, 페르소나 확장을 독립 트랙으로 닫는다.</p></div><div class="mini-metrics"><span>주차: 9-13</span><span>우선순위: P1</span><span>IDOR 검증 필수</span></div></div>
<div class="task-list">
<div class="task-row"><div><span class="task-status s-doing">CHECK</span></div><div><b>OAuth BFF / HttpOnly 세션 완성</b><p>Auth Code + PKCE/state와 opaque HttpOnly/Secure cookie 계약을 회귀화했다. 공개 Google OAuth 실제 <code>/turn</code> proof는 P0 blocker로 남아 있다.</p></div><div><b>산출물</b><p><code>app/test_auth_providers.py</code>, PKCE state store, OAuth callback cookie path</p></div><div><b>검증</b><p>auth URL secret 미노출, browser token 미저장, dev/prod cookie 속성 분리</p></div></div>
<div class="task-row"><div><span class="task-status s-doing">CHECK</span></div><div><b>OAuth BFF / HttpOnly 세션 완성</b><p>Auth Code + PKCE/state와 opaque HttpOnly/Secure cookie 계약을 회귀화했다. OAuth state는 HMAC 서명 토큰과 HttpOnly state cookie를 함께 검증해 API 재시작 뒤 콜백이 <code>invalid_state</code>로 떨어지지 않으면서도 브라우저 시작 흐름에 묶인다. Google provider error는 <code>access_denied</code>/<code>provider_error</code>로 분리하고 로그인 화면에 reason code를 표시한다. 공개 Google OAuth 실제 <code>/turn</code> proof는 P0 blocker로 남아 있다.</p></div><div><b>산출물</b><p><code>app/test_auth_providers.py</code>, cookie-bound signed PKCE state, OAuth callback cookie path, login reason UI</p></div><div><b>검증</b><p>auth URL secret 미노출, browser token 미저장, dev/prod cookie 속성 분리, state cookie 불일치 차단, 재시작 후 signed state 콜백 복구, provider error reason 분리</p></div></div>
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>학습자/교수자/관리자 권한 격리</b><p>runtime fallback에서 타 learner session 직접 접근 403과 learner list 필터를 회귀화했고, teacher/admin durable read audit insert 경로를 추가했다. live Postgres(NOBYPASSRLS <code>vignette_app</code> 역할)로 RLS/audit smoke를 실행해 운영 증거를 확보했다: learner 세션/턴 격리, teacher cohort 스코프, teacher/admin read_session audit insert가 실제 정책 하에서 PASS.</p></div><div><b>산출물</b><p><code>app/test_rbac_idor.py</code>, <code>audit.audit_log</code> read_session insert, <code>scripts/check-postgres-rls-audit.py</code>(live PASS)</p></div><div><b>검증</b><p>타인 <code>session_id</code> 직접 입력 403(unit), live RLS smoke 5 checks PASS</p></div></div>
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b><code>visible_to</code>와 인간 권한 레이어 분리</b><p>learner detail/review와 client engine history에서 evaluator-only turn을 제외한다. live Postgres smoke가 evaluator-only DB 정책을 실제로 검사해 PASS: evaluator-only turn이 learner/client/counselor AI에는 숨겨지고 evaluator AI에만 보인다.</p></div><div><b>산출물</b><p><code>TurnRecord.visible_to</code>, learner/client/evaluator filters, <code>docs/ops/postgres-rls-audit-smoke.md</code></p></div><div><b>검증</b><p>evaluator-only turn 숨김 unit proof + live RLS smoke <code>visible_to.evaluator_only_hidden</code> PASS</p></div></div>
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>페르소나 P2/P3 교수 검수 루프</b><p>approved catalog와 draft/review 교수 검수 큐를 분리하고, teacher/admin 승인·반려 mutation과 교수 콘솔 UI proof를 추가했다. 교수 콘솔의 404 raw 노출도 제거(404→정상 빈상태)했다.</p></div><div><b>산출물</b><p><code>/personas/review</code>, <code>POST /personas/review/{persona_id}</code>, professor review panel</p></div><div><b>검증</b><p><code>app.test_persona_review</code> 14 tests; teacher UI single-run E2E 1 passed; live RLS smoke PASS</p></div></div>
@ -645,7 +743,7 @@
<div class="task-row"><div><span class="task-status s-doing">CHECK</span></div><div><b>Deepgram STT + EOT</b><p>provider-neutral EOT helper로 final transcript와 silence threshold readiness를 회귀화했다. 실제 Deepgram interim/final 스트림 연결은 남아 있다.</p></div><div><b>산출물</b><p><code>assess_end_of_turn</code>, <code>EndOfTurnDecision</code>, EOT silence threshold</p></div><div><b>검증</b><p>final pending, empty transcript, silence threshold 케이스 unit test</p></div></div>
<div class="task-row"><div><span class="task-status s-doing">CHECK</span></div><div><b>OpenAI TTS voice preset 추상화</b><p>P1/P2/P3 persona voice preset, invalid preset fallback, speed clamp, instructions payload 계약을 회귀화했다. DB <code>voice_persona_map</code>과 live TTS 호출은 남아 있다.</p></div><div><b>산출물</b><p><code>resolve_voice</code>, <code>build_tts_payload</code>, provider-neutral params test</p></div><div><b>검증</b><p>P1/P2/P3 voice 분리, fallback, fake-client streaming payload OK</p></div></div>
<div class="task-row"><div><span class="task-status s-plan">POC</span></div><div><b>P1 서연 무참조 synthetic voice 샘플</b><p>Higgs v3 로컬 런타임에서 실존 reference voice 없이 synthetic seed를 먼저 만들고, 그 seed만 재참조해 우울·피로·불안·라포·회복감 5개 변주를 생성했다. 개발용 <code>VIGNETTE_VOICE_POC_SAMPLE_TTS</code> 플래그로 <code>/voice/ws</code> P1 sample TTS provider에도 연결했다.</p></div><div><b>산출물</b><p><code>docs/voice-art/bimec-persona-voice-poc-2026-06-27.md</code>, <code>docs/voice-art/p1-seoyeon-higgs-v3-20260627/manifest.json</code>, <code>apps/api/app/services/voice.py</code></p></div><div><b>검증</b><p>mp3/wav 6개(seed+5) 생성, 24kHz, 7.72~11.68초. live <code>/voice/ws</code> text_turn smoke: provider <code>p1-sample-poc</code>, binary 20 chunks/78,573 bytes. 샘플 provider는 TTS override만 담당하고 마이크/STT는 OpenAI 키가 필요하다. 프로덕션 탑재는 모델 라이선스·권리정책 결정 전 금지.</p></div></div>
<div class="task-row"><div><span class="task-status s-doing">CHECK</span></div><div><b>barge-in / paralinguistic logging</b><p>음성 learner turn에 audio_ref, silence_ms, speech_rate, barge_in을 nullable 메타로 저장한다. 실제 장시간 WSS/마이크 증거는 남아 있다.</p></div><div><b>산출물</b><p><code>turns</code> nullable 확장, voice route 메타 산출</p></div><div><b>검증</b><p>오디오 STT→엔진→TTS 더블 테스트에서 voice turn log에 메타 저장</p></div></div>
<div class="task-row"><div><span class="task-status s-doing">CHECK</span></div><div><b>barge-in / paralinguistic logging</b><p>음성 learner turn에 audio_ref, silence_ms, speech_rate, barge_in을 nullable 메타로 저장하고, 회기 리뷰에서 침묵·발화 속도·끼어듦·음성 입력 비언어 칩으로 표시한다. 실제 장시간 WSS/마이크와 한숨·울음 감지 증거는 남아 있다.</p></div><div><b>산출물</b><p><code>turns</code> nullable 확장, voice route 메타 산출, <code>ReviewNonverbalEvent</code>, <code>SessionReview</code> 칩 UI</p></div><div><b>검증</b><p>오디오 STT→엔진→TTS 더블 테스트에서 voice turn log에 메타 저장; session review E2E 1 passed; backend voice/session 17 passed</p></div></div>
<div class="task-row"><div><span class="task-status s-doing">CHECK</span></div><div><b>s2s 2차 PoC 판단</b><p>캐스케이드 1차 원칙과 s2s 유지/폐기 증거 기준을 문서화했다. 채택 여부 결정은 윤찬 DECIDE로 유지한다.</p></div><div><b>산출물</b><p><code>docs/decisions/voice-s2s-poc.md</code></p></div><div><b>검증</b><p>transcript, safety, audit, latency, failure, privacy 기준 명시</p></div></div>
</div>
</div>
@ -655,19 +753,19 @@
<div class="task-list">
<div class="task-row"><div><span class="task-status s-plan">GATE</span></div><div><b>20명 교육용 파일럿 운영</b><p>20명 파일럿을 실행하기 위한 roster, consent, session completion evidence gate를 문서화했다. 실제 참여자 완주 증거는 아직 필요하다.</p></div><div><b>산출물</b><p><code>docs/phase3/pilot-runbook.md</code>, evidence root layout, operator gates</p></div><div><b>검증</b><p><code>scripts/check-phase3-artifacts.py --help</code>; 실제 evidence root validation은 파일럿 시점에 실행</p></div></div>
<div class="task-row"><div><span class="task-status s-plan">GATE</span></div><div><b>효과성/KPI 측정</b><p>SUS, 자기효능감, κ/ICC, 환각률, 피드백 타당도 기준과 report schema를 고정했다. 실제 수치 충족 여부는 파일럿 데이터 이후 판단한다.</p></div><div><b>산출물</b><p><code>docs/phase3/kpi-measurement-plan.md</code>, KPI report schema</p></div><div><b>검증</b><p>checker가 <code>prepost_measures</code>, <code>sus_responses</code>, <code>kpi_report.json</code> evidence를 요구</p></div></div>
<div class="task-row"><div><span class="task-status s-plan">GATE</span></div><div><b>재귀학습 데이터셋 1차 산출</b><p>anonymized JSONL export manifest와 누수/식별자 점검 기준을 문서화했다. 실제 export 승인과 데이터 steward 검수는 남아 있다.</p></div><div><b>산출물</b><p><code>docs/phase3/dataset-export-manifest.md</code>, <code>03-export/export_manifest.json</code> contract</p></div><div><b>검증</b><p>source/hash/consent/withdrawal/anonymization fields 요구, raw identifier 금지</p></div></div>
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>재귀학습 데이터셋 1차 산출</b><p>masked-text JSONL dry-run exporter, PII scan, κ/ICC 계산, approved export 차단 게이트를 구현했다. 실제 approved export와 데이터 steward 검수는 남아 있다.</p></div><div><b>산출물</b><p><code>scripts/export-recursive-dataset.py</code>, <code>app.services.dataset_export</code>, <code>docs/phase3/dataset-export-manifest.md</code></p></div><div><b>검증</b><p><code>pytest app/test_dataset_export.py</code> 5 passed, DB dry-run 5 rows → <code>technical_dry_run</code> manifest + sha256 생성, PII finding 시 approved 불가</p></div></div>
<div class="task-row"><div><span class="task-status s-plan">GATE</span></div><div><b>개인정보/동의 감사</b><p>동의, 철회, privacy audit, 해외 전송 고지 evidence checklist를 만들었다. 실제 법무/개인정보 검토 서명은 파일럿 전 gate로 남아 있다.</p></div><div><b>산출물</b><p><code>docs/phase3/privacy-consent-audit.md</code>, withdrawal log contract</p></div><div><b>검증</b><p>동의 철회 후 session/export 제외 evidence를 checker 입력으로 요구</p></div></div>
</div>
</div>
<div class="tab-panel" id="panel-ops" role="tabpanel" aria-labelledby="tab-ops" hidden>
<div class="detail-head"><div><h3>운영 / 검증 상세</h3><p>실제 운영 도메인에 반영됐는지, 서버가 하나로 정리됐는지, 완료라고 말할 증거가 있는지 확인하는 탭이다.</p></div><div class="mini-metrics"><span>웹: 200</span><span>API: prod OK</span><span>Live2D: 404</span></div></div>
<div class="task-list">
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>운영 웹 asset 확인</b><p>커스텀 도메인이 최신 JS/CSS asset을 서빙한다.</p></div><div><b>산출물</b><p><code>index-D1Z5DH5J.js</code>, <code>index-BA9-xxfn.css</code></p></div><div><b>검증</b><p><code>Invoke-WebRequest https://vignette.chanpaca.net/</code></p></div></div>
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>운영 웹 asset 확인</b><p>커스텀 도메인이 최신 JS/CSS asset을 서빙한다.</p></div><div><b>산출물</b><p><code>index-BzQmv-jU.js</code>, <code>index-CCZ1bDA9.css</code></p></div><div><b>검증</b><p><code>Invoke-WebRequest https://vignette.chanpaca.net/login?probe=deploy-20260627</code></p></div></div>
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>공개 API prod health</b><p>터널은 prod API 8001을 보고, API는 DB와 engine을 모두 확인한다.</p></div><div><b>산출물</b><p><code>environment:"prod"</code>, <code>db:true</code>, <code>engine:true</code></p></div><div><b>검증</b><p><code>https://api-vignette.chanpaca.net/health</code></p></div></div>
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>Tailscale Tailnet 런타임</b><p>로컬 web/api/engine을 Tailscale Serve로 묶어 같은 Tailnet PC·모바일에서 HTTPS로 접근한다. Vite proxy와 API 포트가 어긋나지 않도록 <code>dev-up.ps1</code>에서 <code>VITE_API_PROXY_TARGET</code><code>ApiPort</code>와 동기화한다.</p></div><div><b>산출물</b><p><code>scripts/start-tailscale-runtime.ps1</code>, <code>AUTH_DEV_LOGIN_EXTRA_ORIGINS</code>, <code>VITE_ALLOWED_HOSTS</code></p></div><div><b>검증</b><p><code>https://alpaca-home.taile93291.ts.net/login</code> 200, <code>/api/auth/config</code> dev_login true, dev-login 후 <code>/api/auth/me</code> 200, 로그인 DOM에 <code>로컬 테스트 계정으로 계속</code> 표시.</p></div></div>
<div class="task-row"><div><span class="task-status s-doing">BLOCKED</span></div><div><b>공개 vnet.18ka.net 런타임</b><p><code>vnet.18ka.net</code><code>api-vnet.18ka.net</code>을 정식 공개 도메인으로 허용하도록 코드 경로는 보강했다.</p></div><div><b>산출물</b><p><code>FRONTEND_ORIGIN_MAP</code>, 공개 host별 API 매핑, <code>scripts/start-public-runtime.ps1</code> vnet ingress 옵션</p></div><div><b>블로커</b><p>현재 DNS 미해결, Cloudflare credential의 18ka.net zone 권한 불일치, prod DB <code>127.0.0.1:55432</code> down, Google OAuth redirect URI 등록 필요.</p></div></div>
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>서버 정리 상태</b><p>운영 확인용 프로세스만 유지하고 임시 dev server는 내린다.</p></div><div><b>산출물</b><p>8001 API, 9099 engine, 20241 cloudflared</p></div><div><b>검증</b><p><code>Get-NetTCPConnection</code> 대상 포트 확인</p></div></div>
<div class="task-row"><div><span class="task-status s-doing">CHECK</span></div><div><b>재부팅 후 런타임 재기동</b><p>engine gateway, prod API, cloudflared를 health-check하고 복구하는 watchdog과 Scheduled Task installer를 추가했다. 실제 재부팅 후 public <code>/turn</code> smoke는 남아 있다.</p></div><div><b>산출물</b><p><code>watch-public-runtime.ps1</code>, <code>install-public-runtime-task.ps1</code>, ops runbook</p></div><div><b>검증</b><p>PowerShell parser OK, check-only watchdog 실행</p></div></div>
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>Tailscale Tailnet 런타임</b><p>로컬 web/api/engine을 Tailscale Serve로 묶어 같은 Tailnet PC·모바일에서 HTTPS로 접근한다. Vite proxy와 API 포트가 어긋나지 않도록 <code>dev-up.ps1</code>에서 <code>VITE_API_PROXY_TARGET</code><code>ApiPort</code>와 동기화한다. Tailnet dev-origin에서는 public OAuth redirect_uri가 로컬/Tailnet 세션이 아니라 공개 API 세션으로 돌아가므로 Google 버튼과 직접 Google 시작 URL을 <code>local_oauth_unavailable</code>로 차단하고 dev-login만 사용한다. Vite proxy는 원래 프론트 host/proto를 <code>X-Forwarded-Host</code>/<code>X-Forwarded-Proto</code>로 넘긴다.</p></div><div><b>산출물</b><p><code>scripts/start-tailscale-runtime.ps1</code>, <code>AUTH_DEV_LOGIN_EXTRA_ORIGINS</code>, <code>VITE_ALLOWED_HOSTS</code>, <code>Login.tsx</code>, <code>vite.config.ts</code>, <code>auth.py</code></p></div><div><b>검증</b><p><code>https://alpaca-home.taile93291.ts.net/login</code> 200, <code>/api/auth/config</code> dev_login true, dev-login 후 <code>/api/auth/me</code> 200, direct Google start → <code>/login?oauth=local_oauth_unavailable</code>, Tailnet auth/dev-login E2E 2 passed.</p></div></div>
<div class="task-row"><div><span class="task-status s-doing">BLOCKED</span></div><div><b>공개 vnet.18ka.net 런타임</b><p><code>vnet.18ka.net</code><code>api-vnet.18ka.net</code>을 정식 공개 도메인으로 허용하도록 코드 경로는 보강했다.</p></div><div><b>산출물</b><p><code>FRONTEND_ORIGIN_MAP</code>, 공개 host별 API 매핑, <code>scripts/start-public-runtime.ps1</code> vnet ingress 옵션</p></div><div><b>블로커</b><p>현재 DNS 미해결, Cloudflare credential의 18ka.net zone 권한 불일치, Google OAuth redirect URI 등록 필요.</p></div></div>
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>서버 정리 상태</b><p>운영 API와 로컬/Tailnet 테스트 API를 구분해 유지한다.</p></div><div><b>산출물</b><p>8001 prod API, 8000 local API, 8010 Tailnet API, 5173 web, 9099 engine, 20241 cloudflared</p></div><div><b>검증</b><p><code>Get-NetTCPConnection</code> 대상 포트 확인</p></div></div>
<div class="task-row"><div><span class="task-status s-doing">CHECK</span></div><div><b>재부팅 후 런타임 재기동</b><p>engine gateway, prod API, cloudflared를 health-check하고 복구하는 watchdog과 Scheduled Task installer를 추가했다. 아직 DNS가 없는 <code>api-vnet.18ka.net</code>은 기본 watchdog 검사에서 제외해 불필요한 restart loop를 막고, 실제 개통 후 <code>-AdditionalPublicHealthUrls</code>로 명시 추가한다. 실제 재부팅 후 public <code>/turn</code> smoke는 남아 있다.</p></div><div><b>산출물</b><p><code>watch-public-runtime.ps1</code>, <code>install-public-runtime-task.ps1</code>, ops runbook</p></div><div><b>검증</b><p>PowerShell parser OK, check-only watchdog healthy: engine/api/web-preview/cloudflared/public-api</p></div></div>
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>레이아웃 감사팀 / 전면 레이아웃 개선</b><p>서브에이전트 전담으로 1차 overflow/clipping 수리 후 2차 전면 재설계를 진행했고, 이번 라운드에서 7개 권장 너비 시각 수용까지 닫았다. 세션, 학습자 홈/리뷰, 교수 콘솔, 관리자, 설정 화면을 작업 화면 기준으로 재배치했고, 과한 빈칸·튀어나감·버튼/텍스트 잘림·모바일 테이블 사용성을 줄였다. 엄격한 시각 게이트 + 화면별 적대적 시각 재검수에서 7개 화면 모두 accept(critical/major 0, 회귀 0)를 받아 DONE으로 닫는다.</p></div><div><b>산출물</b><p><code>docs/ops/layout-redesign-handoff-2026-06-26.md</code>, page-by-page redesign patches, <code>apps/web/e2e/layout-visual-gate.spec.ts</code></p></div><div><b>검증</b><p><code>npm run typecheck</code> OK, 엄격 시각 게이트 7 passed, layout focused E2E desktop/mobile 54 passed, 적대적 시각 재검수 7/7 accept·회귀 0.</p></div></div>
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>엄격한 레이아웃 시각 게이트 구축 · 병렬 수정 · 적대적 재검수</b><p>핸드오프가 요구한 7개 권장 너비(390/720/861/900/1024/1280/1440) 시각 수용을 자동 게이트로 고정했다. 7개 재설계 화면을 각 너비에서 렌더링해 가로 overflow 0, 컨트롤 하드클립/텍스트클립 0을 강제하고 화면당 7장 총 49장 풀페이지 스크린샷을 남긴다. 화면별 1에이전트 병렬 시각 리뷰로 49장 직접 판독 → 6개 병렬 수정 에이전트(파일 비중첩)가 결함 수정 → 화면별 적대적 재검수로 해소·무회귀 확정.</p></div><div><b>산출물</b><p><code>apps/web/e2e/layout-visual-gate.spec.ts</code>, <code>node_modules/.tmp/layout-gate/*.png</code> 49장, 2× workflow(review/verify)·6× fix agent</p></div><div><b>검증</b><p>게이트 7 passed(재스크린샷). 해소된 핵심 결함: 학습자홈 1280/1440 3열 가운데 컬럼 붕괴(critical → 2열 분기 상향 + word-break:keep-all), 교수 'API 404' raw 배너 제거, 리뷰 빈상태 위계 역전, 관리자 스켈레톤, 설정 와이드 좌측 데드존 제거, 세션 모바일 44px 터치타깃. 적대적 재검수 7/7 accept, regression 0.</p></div></div>
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>레이아웃 cosmetic minor 폴리시 일괄 처리</b><p>적대적 재검수 잔여 cosmetic minor를 5개 병렬 폴리시 에이전트로 처리했다. 학습자홈 헬퍼문구 고아 글자, 세션 1024 일시정지 아이콘 정리 + 보내기 버튼 대비 강화, 설정 2x2 칩 행 높이 균일, 교수 검토 큐 카드 갭 제거, 관리자 2열 높이 동기화를 적용했다.</p></div><div><b>산출물</b><p>page-by-page polish patches(LearnerHome/Session/Settings/Professor/Admin)</p></div><div><b>검증</b><p><code>npm run typecheck</code> OK, 시각 게이트 7 passed, 레이아웃 E2E desktop/mobile 54 passed, session-layout 8/8 — 회귀 0.</p></div></div>
@ -678,19 +776,25 @@
<thead><tr><th>범위</th><th>명령 / 확인 대상</th><th>결과</th></tr></thead>
<tbody>
<tr><td>Web typecheck</td><td><code>npm run typecheck</code></td><td>Passed</td></tr>
<tr><td>Backend unittest</td><td><code>python -m unittest app.test_runtime_policy app.test_session_turn_persistence app.test_evaluation_persistence app.test_state_machine_resistance app.test_orchestrator_masking app.test_rbac_idor app.test_voice_service app.test_auth_providers app.test_voice_ws app.test_persona_review engine_gateway.test_gateway_model</code></td><td>98 tests OK</td></tr>
<tr><td>API typegen SSOT</td><td><code>npm run check:api-types</code></td><td>Passed; FastAPI OpenAPI → <code>src/lib/api.gen.ts</code> stale check</td></tr>
<tr><td>Backend pytest</td><td><code>python -m pytest app/ -q</code></td><td>119 passed</td></tr>
<tr><td>C1 case worksheet</td><td><code>python -m pytest app/test_session_turn_persistence.py -q</code> / <code>session-review.spec.ts</code></td><td>14 passed; session review exposes transcript-grounded <code>caseWorksheet</code> and UI evidence jump</td></tr>
<tr><td>M3 auth claim mapping</td><td><code>python -m pytest app/test_auth_providers.py -q</code></td><td>22 passed; Google/SAML cohort maps, provider external_id, provider error reason covered</td></tr>
<tr><td>CrisisGate 109</td><td><code>python -m pytest app/test_session_turn_persistence.py app/test_voice_ws.py -q</code></td><td>20 passed; real crisis stops before engine, returns 109 resource, safety event DB insert payload covered</td></tr>
<tr><td>Professor learner growth</td><td><code>python -m pytest app/test_teacher_dashboard.py app/test_session_turn_persistence.py app/test_voice_ws.py -q</code></td><td>21 passed; teacher dashboard exposes learner_growth and safety alert queue</td></tr>
<tr><td>Persona authoring CRUD</td><td><code>python -m pytest app/test_persona_review.py -q</code></td><td>19 passed; teacher/admin draft create/read/update/review submit, learner blocked, approved-only catalog maintained</td></tr>
<tr><td>P1 cost telemetry</td><td><code>app.test_session_turn_persistence.SessionTurnPersistenceTest</code></td><td>Passed; generate/stream client AI turns store provider/model/tokens/cost</td></tr>
<tr><td>Engine stream contract</td><td><code>gateway token/done/error parsing</code></td><td>Passed; gateway error event does not create saved turns</td></tr>
<tr><td>Engine session reuse</td><td><code>engine_gateway.test_gateway_model</code></td><td>7 tests OK; live session_id reuse and ephemeral close fixed</td></tr>
<tr><td>P1 masking gate</td><td><code>app.test_orchestrator_masking</code></td><td>4 tests OK; phone/email/RRN absent from generate/stream payloads</td></tr>
<tr><td>P2a RBAC/audit/visibility</td><td><code>app.test_rbac_idor</code></td><td>7 tests OK; other learner 403, read_session audit, evaluator-only hidden</td></tr>
<tr><td>Turn evaluation SSOP</td><td><code>python -B -m unittest app.test_session_turn_persistence app.test_evaluation_persistence</code></td><td>14 tests OK; fast-loop evaluation normalized mapping + evaluator-only raw RLS + turn insert id 확인</td></tr>
<tr><td>Turn evaluation SSOP</td><td><code>pytest app/test_session_turn_persistence.py app/test_evaluation_persistence.py app/test_voice_ws.py</code></td><td>20 tests OK; submit/stream/voice fast-loop evaluation normalized mapping + evaluator-only raw RLS + turn insert id 확인</td></tr>
<tr><td>Postgres RLS/audit smoke checker</td><td><code>python scripts\check-postgres-rls-audit.py --help</code></td><td>py_compile/help/safe default OK; live DSN run still required for proof</td></tr>
<tr><td>OAuth/BFF cookie security</td><td><code>app.test_auth_providers</code></td><td>12 tests OK; PKCE/state, opaque HttpOnly/Secure cookie, no browser tokens, SAML AuthnRequest/dev-only fixture ACS</td></tr>
<tr><td>Persona review workflow</td><td><code>app.test_persona_review</code></td><td>14 tests OK; approved-only catalog, teacher/admin queue, approve/reject audit, learner 403</td></tr>
<tr><td>OAuth/BFF cookie security</td><td><code>app.test_auth_providers</code></td><td>20 tests OK; PKCE/state, cookie-bound signed state restart recovery, state cookie mismatch reject, opaque HttpOnly/Secure cookie, no browser tokens, SAML AuthnRequest/dev-only fixture ACS, dev/Tailnet OAuth guard, public-origin dev-login closed</td></tr>
<tr><td>Persona review workflow</td><td><code>app.test_persona_review</code></td><td>19 tests OK; approved-only catalog, teacher/admin queue, draft authoring, approve/reject audit, learner 403</td></tr>
<tr><td>Voice WS contract</td><td><code>app.test_voice_ws</code></td><td>5 tests OK; auth guard, audio_start/chunks/audio_end, text_turn, ping, max audio cap</td></tr>
<tr><td>Voice preset/EOT</td><td><code>app.test_voice_service</code></td><td>11 tests OK; TTS payload, preset fallback, P1 sample TTS, EOT readiness</td></tr>
<tr><td>Voice metadata</td><td><code>app.test_session_turn_persistence</code></td><td>audio_ref/silence_ms/speech_rate/barge_in persisted on learner voice turn</td></tr>
<tr><td>Voice metadata</td><td><code>app.test_session_turn_persistence</code></td><td>audio_ref/silence_ms/speech_rate/barge_in persisted on learner voice turn; review <code>nonverbal</code> events exposed</td></tr>
<tr><td>P1 MVP UI E2E</td><td><code>npx playwright test e2e/session-mvp.spec.ts --project=chromium-single-run</code></td><td>1 passed</td></tr>
<tr><td>Persona review UI E2E</td><td><code>npx playwright test e2e/teacher.spec.ts --project=chromium-single-run</code></td><td>1 passed; teacher approves pending persona from console</td></tr>
<tr><td>Voice UI synthetic E2E</td><td><code>npx playwright test e2e/voice-success.spec.ts --project=chromium-single-run</code></td><td>2 passed; direct WS cascade plus Session mic button path with synthetic browser audio</td></tr>
@ -700,10 +804,10 @@
<tr><td>Python compile</td><td><code>python -m compileall app engine_gateway</code></td><td>Passed</td></tr>
<tr><td>Phase 3 artifact gates</td><td><code>python scripts\check-phase3-artifacts.py --help</code></td><td>Passed; runbook, KPI plan, dataset manifest, privacy audit checker documented</td></tr>
<tr><td>Web build</td><td><code>npm run build</code></td><td>Passed</td></tr>
<tr><td>Pages production deploy</td><td><code>wrangler pages deploy dist --project-name vignette --branch main</code></td><td><code>0fe8f3cc</code>, source <code>543d48c</code></td></tr>
<tr><td>Custom domain assets</td><td><code>https://vignette.chanpaca.net/</code></td><td><code>index-D1Z5DH5J.js</code>, <code>index-BA9-xxfn.css</code></td></tr>
<tr><td>Pages production deploy</td><td><code>wrangler pages deploy dist --project-name vignette --branch main --commit-dirty=true</code></td><td><code>2a58ca49</code>, source <code>6bd91b0d</code> + dirty worktree</td></tr>
<tr><td>Custom domain assets</td><td><code>https://vignette.chanpaca.net/login?probe=deploy-20260627</code></td><td><code>index-BzQmv-jU.js</code>, <code>index-CCZ1bDA9.css</code></td></tr>
<tr><td>Legacy Live2D routes</td><td><code>/live2d/mao/* / /live2d/haru/* / /live2d/live2dcubismcore.min.js</code></td><td>404</td></tr>
<tr><td>Compose</td><td><code>docker compose --env-file .env.example config --quiet</code></td><td>Passed with dummy required env</td></tr>
<tr><td>Compose</td><td><code>docker compose -f infra\docker-compose.yml --env-file infra\.env.example config --quiet</code></td><td>Passed with dummy required env</td></tr>
<tr><td>Focused E2E</td><td><code>admin + db-persistence + voice-success</code></td><td>10 passed</td></tr>
<tr><td>Admin manage-users</td><td><code>admin.spec.ts --grep manage real server-known users</code></td><td>desktop/mobile 2 passed</td></tr>
<tr><td>Full E2E</td><td><code>PLAYWRIGHT_HOST=127.0.0.1 npx playwright test</code></td><td>72 passed</td></tr>
@ -711,8 +815,9 @@
<tr><td>Public readiness</td><td><code>public-auth-turn.spec.ts --grep production-safe</code></td><td>1 passed</td></tr>
<tr><td>Persona auth boundary</td><td><code>GET /personas</code></td><td>verified local/public unauth 401</td></tr>
<tr><td>Public login</td><td><code>auth.spec.ts --grep public login</code></td><td>1 passed</td></tr>
<tr><td>Public runtime scripts</td><td><code>start/watch/install-public-runtime*.ps1</code></td><td>parser OK; watchdog check-only path verified</td></tr>
<tr><td>Public runtime scripts</td><td><code>start/watch/install-public-runtime*.ps1</code></td><td>parser OK; watchdog check-only healthy. Default public checks exclude not-yet-live <code>api-vnet.18ka.net</code>; add future domains explicitly with <code>-AdditionalPublicHealthUrls</code>.</td></tr>
<tr><td>Public API health</td><td><code>https://api-vignette.chanpaca.net/health</code></td><td>prod, db true, engine true</td></tr>
<tr><td>Public/local/Tailnet login recovery</td><td><code>https://vignette.chanpaca.net/login</code> / <code>http://localhost:5173/login</code> / <code>https://alpaca-home.taile93291.ts.net/login</code></td><td>public login 200, local/Tailnet login 200, public Google auth redirect 302 with HttpOnly state cookie, cookie-bound signed OAuth state survives API restart(fake code → <code>token_exchange_failed</code>, not <code>invalid_state</code>), provider callback error → <code>oauth=access_denied</code>, OAuth callback 실패는 <code>api.public.err.log</code>에 reason/status만 비밀값 없이 남김, 로그인 화면은 reason code 표시, local/Tailnet Google direct → <code>local_oauth_unavailable</code>, local auth E2E 7 passed, Tailnet auth/dev-login E2E 2 passed</td></tr>
<tr><td>Local 5175 login</td><td><code>PLAYWRIGHT_BASE_URL=http://127.0.0.1:5175 auth.spec.ts</code></td><td>desktop/mobile passed</td></tr>
<tr><td>Learner/readiness E2E</td><td><code>learner.spec.ts + readiness.spec.ts desktop/mobile</code></td><td>14 passed</td></tr>
<tr><td>Learner screenshots</td><td><code>learn-empty-desktop.png / learn-empty-mobile-compact.png</code></td><td>document overflow 0, empty history visible</td></tr>
@ -745,7 +850,7 @@ $env:E2E_PUBLIC_STORAGE_STATE=".\node_modules\.tmp\public-auth.json"
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\start-public-runtime.ps1</pre>
</div>
<div class="source-note"><b>닫은 항목(live 증거):</b> ① engine config 운영값(<code>claude_cli / 9099 / gateway-default</code>, durable, db). ② 상주 엔진풀 probe — 단일 session_id 2회 reused stream, TTFT 16674199ms, cost 누적 0.068→0.123. ③ Postgres RLS/audit smoke 5 checks PASS. ④ turn cost telemetry — <code>app.turns</code> client_ai 13행에 provider/model/cost_usd 실적재(합 $1.22). ⑤ 레이아웃 시각 게이트 7/7 + 적대적 재검수 7/7 accept.</div>
<div class="source-note"><b>환경 제약으로 이 세션에서 못 닫는 항목(정직 표기):</b> 공개 Google OAuth 실제 <code>/turn</code>(owner 지시로 보류) · <code>vnet.18ka.net</code>/<code>api-vnet.18ka.net</code> 공개 런타임(DNS 미해결, Cloudflare 18ka.net zone 권한 불일치, prod DB 55432 down, Google OAuth redirect URI 등록 필요) · 한신대 데이터/SSO 거버넌스(owner 지시로 보류) · 음성 live STT/TTS provider·물리 마이크·공개 WSS 50분 · 저항엔진 openness 곡선 DB 실증 · claude_cli↔Messages API 폴백 동일성 · 재부팅 후 watchdog smoke · Phase 3 파일럿 게이트 · decisions 6건(owner 결정). 운영 원칙상 가짜 증거로 DONE 표기하지 않는다. 전체 추적: <code>docs/ops/backlog-2026-06-26.md</code>(B1 폴리시 · B2 환경제약 · B3 소유자결정 · B4 거버넌스).</div>
<div class="source-note"><b>환경 제약으로 이 세션에서 못 닫는 항목(정직 표기):</b> 공개 Google OAuth 실제 <code>/turn</code>(owner 지시로 보류) · <code>vnet.18ka.net</code>/<code>api-vnet.18ka.net</code> 공개 런타임(DNS 미해결, Cloudflare 18ka.net zone 권한 불일치, Google OAuth redirect URI 등록 필요) · 한신대 데이터/SSO 거버넌스(owner 지시로 보류) · 음성 live STT/TTS provider·물리 마이크·공개 WSS 50분 · 저항엔진 openness 곡선 DB 실증 · claude_cli↔Messages API 폴백 동일성 · 재부팅 후 watchdog smoke · Phase 3 파일럿 게이트 · decisions 6건(owner 결정). 운영 원칙상 가짜 증거로 DONE 표기하지 않는다. 전체 추적: <code>docs/ops/backlog-2026-06-26.md</code>(B1 폴리시 · B2 환경제약 · B3 소유자결정 · B4 거버넌스).</div>
</div>
<div class="tab-panel" id="panel-decisions" role="tabpanel" aria-labelledby="tab-decisions" hidden>
<div class="detail-head"><div><h3>윤찬 결정 필요 상세</h3><p>람다가 단독으로 정하면 월권인 항목이다. 결정되기 전에는 "완료"가 아니라 조건부로 둔다.</p></div><div class="mini-metrics"><span>결정 6개</span><span>조건부 GO 사유</span></div></div>
@ -771,7 +876,7 @@ powershell -NoProfile -ExecutionPolicy Bypass -File scripts\start-public-runtime
<summary><span class="fcaret" aria-hidden="true"></span>이번 세션 내러티브 (원문 보존)<span class="fmeta">team lead review</span></summary>
<div class="fold-inner">
<p style="color:var(--ink-2);font-size:13px;max-width:840px">
이번 세션은 seed persona 결합 제거, 비개발 fail-closed 설정, compose OAuth 주입, admin 모바일 overflow 수정, persona 인증 경계 보강, 전체 Playwright 재통과, 공개 API prod 전환까지 완료했다. 이번 이어받기에서는 내담자 AI 생성 턴의 provider/model/token/cost 텔레메트리를 <code>/turn</code>, SSE stream, 음성 생성 경로에서 DB <code>app.turns</code>까지 적재하도록 연결했고, gateway stream <code>done/error</code> 계약을 구조화했으며, P1 상담 시작→텍스트 stream→종료→리뷰 피드백 UI E2E와 저항 엔진 정량 회귀 테스트를 추가했다. 이어서 PII 마스킹 게이트 우회 차단, RBAC/IDOR·감사 insert 경로, 음성 paralinguistic 메타 저장, 공개 런타임 watchdog 스크립트와 runbook을 보강했다. 추가로 상주 engine session reuse, SAML provider 스캐폴드, <code>visible_to</code> 런타임 필터, EOT/TTS payload 계약까지 회귀화했다. 백엔드 회귀 테스트는 84개로 재검증했고, 페르소나 승인/반려 API와 교수 콘솔 검수 UI proof를 추가했다. Postgres RLS/audit live smoke checker와 s2s 2차 PoC 판단 메모도 준비했지만, live DB 실행과 s2s 채택 결정은 별도 증거가 필요하다. 이후 로컬 5175 런타임 재기동, admin 사용자 관리 E2E 안정화, 학습자 홈 0회 기록 UX, active session 실제 SSE 스트림 전환, 텍스트/음성 실패 턴 미저장, 직접 URL persona 우회 차단, Live2D demo 자산 제거, AI 운영 fail-closed 보강까지 전체 Playwright로 확인했다. 2026-06-26 15:18 KST 기준 공개 API 터널을 복구한 뒤 전체 Playwright 72개가 desktop/mobile/single-run 모두 통과했다. Cloudflare Pages production 배포 <code>0fe8f3cc</code>도 완료했고, 커스텀 도메인이 새 asset을 서빙하는 것을 확인했다. 과거 Mao/Haru Live2D demo 자산은 repo와 번들에서 제거했고, <code>/live2d/*</code> 운영 경로는 404로 차단했다. 로컬 프로세스는 prod API 8001, engine gateway 9099, cloudflared tunnel 1개만 남기고 dev API와 임시 Vite preview를 내렸다. 이번 라운드에서는 레이아웃 작업을 서브에이전트 전담으로 분리해 세션, 학습자 홈/리뷰, 관리자/교수자, 설정 화면을 overflow 수리 수준이 아니라 실제 작업 화면 재설계 수준으로 갱신했다. 통합 레이아웃 E2E는 desktop/mobile 54개를 최종 통과했고, 자세한 인수인계는 <code>docs/ops/layout-redesign-handoff-2026-06-26.md</code>에 남겼다. 이번 이어받기에서는 핸드오프가 남긴 "실기기 시각 수용"을 엄격한 자동 게이트로 닫았다. 7개 권장 너비(390/720/861/900/1024/1280/1440)에서 7개 재설계 화면의 가로 overflow 0·컨트롤 클립 0을 강제하는 <code>layout-visual-gate.spec.ts</code>(7 passed, 49장 스크린샷)를 만들고, 화면별 1에이전트 병렬 시각 리뷰 → 6개 병렬 수정 에이전트 → 화면별 적대적 재검수까지 돌려 7/7 accept(critical/major 0, 회귀 0)를 확정했다. 학습자홈 1280 3열 가운데 컬럼 붕괴(critical)와 교수 'API 404' raw 배너를 포함한 결함을 모두 해소했고, 레이아웃 항목을 DONE으로 닫았다. 다만 이 HTML은 지금까지 Phase 0 상태판처럼 쓰였고, 아래에 Phase 1 이후 전체 계획을 보강했다. 공개 Google OAuth 실제 <code>/turn</code> 증명만 storageState 캡처 후 남아 있다.
이번 세션은 seed persona 결합 제거, 비개발 fail-closed 설정, compose OAuth 주입, admin 모바일 overflow 수정, persona 인증 경계 보강, 전체 Playwright 재통과, 공개 API prod 전환까지 완료했다. 이번 이어받기에서는 내담자 AI 생성 턴의 provider/model/token/cost 텔레메트리를 <code>/turn</code>, SSE stream, 음성 생성 경로에서 DB <code>app.turns</code>까지 적재하도록 연결했고, gateway stream <code>done/error</code> 계약을 구조화했으며, P1 상담 시작→텍스트 stream→종료→리뷰 피드백 UI E2E와 저항 엔진 정량 회귀 테스트를 추가했다. 이어서 PII 마스킹 게이트 우회 차단, RBAC/IDOR·감사 insert 경로, 음성 paralinguistic 메타 저장, 공개 런타임 watchdog 스크립트와 runbook을 보강했다. 추가로 상주 engine session reuse, SAML provider 스캐폴드, <code>visible_to</code> 런타임 필터, EOT/TTS payload 계약까지 회귀화했다. 백엔드 회귀 테스트는 84개로 재검증했고, 페르소나 승인/반려 API와 교수 콘솔 검수 UI proof를 추가했다. Postgres RLS/audit live smoke checker와 s2s 2차 PoC 판단 메모도 준비했지만, live DB 실행과 s2s 채택 결정은 별도 증거가 필요하다. 이후 로컬 5175 런타임 재기동, admin 사용자 관리 E2E 안정화, 학습자 홈 0회 기록 UX, active session 실제 SSE 스트림 전환, 텍스트/음성 실패 턴 미저장, 직접 URL persona 우회 차단, Live2D demo 자산 제거, AI 운영 fail-closed 보강까지 전체 Playwright로 확인했다. 2026-06-26 15:18 KST 기준 공개 API 터널을 복구한 뒤 전체 Playwright 72개가 desktop/mobile/single-run 모두 통과했다. Cloudflare Pages production 배포 <code>2a58ca49</code>도 완료했고, 커스텀 도메인이 새 asset을 서빙하는 것을 확인했다. 과거 Mao/Haru Live2D demo 자산은 repo와 번들에서 제거했고, <code>/live2d/*</code> 운영 경로는 404로 차단했다. 현재 런타임은 prod API 8001, dev API 8000/8010, web 5173, engine gateway 9099, cloudflared tunnel 1개를 의도적으로 유지한다. 이번 라운드에서는 레이아웃 작업을 서브에이전트 전담으로 분리해 세션, 학습자 홈/리뷰, 관리자/교수자, 설정 화면을 overflow 수리 수준이 아니라 실제 작업 화면 재설계 수준으로 갱신했다. 통합 레이아웃 E2E는 desktop/mobile 54개를 최종 통과했고, 자세한 인수인계는 <code>docs/ops/layout-redesign-handoff-2026-06-26.md</code>에 남겼다. 이번 이어받기에서는 핸드오프가 남긴 "실기기 시각 수용"을 엄격한 자동 게이트로 닫았다. 7개 권장 너비(390/720/861/900/1024/1280/1440)에서 7개 재설계 화면의 가로 overflow 0·컨트롤 클립 0을 강제하는 <code>layout-visual-gate.spec.ts</code>(7 passed, 49장 스크린샷)를 만들고, 화면별 1에이전트 병렬 시각 리뷰 → 6개 병렬 수정 에이전트 → 화면별 적대적 재검수까지 돌려 7/7 accept(critical/major 0, 회귀 0)를 확정했다. 학습자홈 1280 3열 가운데 컬럼 붕괴(critical)와 교수 'API 404' raw 배너를 포함한 결함을 모두 해소했고, 레이아웃 항목을 DONE으로 닫았다. 이번 로그인 복구 라운드에서는 dev/Tailnet OAuth direct start까지 <code>local_oauth_unavailable</code>로 막고, public OAuth state를 HttpOnly 쿠키에 묶인 서명 토큰으로 바꿔 API 재시작 후 콜백이 <code>invalid_state</code>로 실패하지 않게 했으며, callback 실패 reason/status 진단 로그와 화면 메시지 분기를 추가했다. 추가로 Google provider error를 <code>access_denied</code>/<code>provider_error</code>로 분리하고 로그인 화면에 reason code를 노출했다. 이어서 C1 사례개념화 워크시트 1차 구조를 리뷰 API와 화면에 붙여 축어록 근거 기반 read-only 초안을 제공한다. 공개 Google OAuth 실제 <code>/turn</code> 증명만 storageState 캡처 후 남아 있다.
</p>
</div>
</details>

View file

@ -222,8 +222,10 @@ RBAC×AIView로 차단된다. 이 모듈은 평가 신호만 산출한다.
회기 라이프사이클 메모리(4계층 매핑: ① working / ② episodic / ③ summary / ④ semantic).
- 회기 시작: `build_recall_context(...) -> RecallContext` — 큰그림(case_digest) → 직전 요약 →
episodic 단편 순으로 회상 조립. **회상 요약엔 CCD·정답·평가가 절대 들어가지 않는다**(내담자 뷰, M6).
- 회기 시작: `(persona_id, learner_id)` 안정 `case_profile`을 확보한 뒤
`build_recall_context(...) -> RecallContext`를 조립한다. 현재 동기 seed는 직전
`session_summary` 기반이고, episodic 단편/KB 단서는 백그라운드 warm cache로 붙는다.
**회상 요약엔 CCD·정답·평가가 절대 들어가지 않는다**(내담자 뷰, M6).
DB/RAG가 없으면 인자 None → 빈 RecallContext(첫 회기/in-proc 폴백).
- 회기 종료: `make_carry_over(state, ...) -> CarryOver` — (A) `end_state = state.snapshot()`
무손실 코드 복사(P4) (B) `rapport_delta` (C) 서사 digest는 `CompressionJob`으로 큐잉(LLM 비동기,
@ -257,8 +259,9 @@ server: ready → state(listening) → state(thinking) → transcript → reply
핵심 엔드포인트:
- `POST /sessions``get_catalog_persona(code)`로 승인 카드 조회 → `memory.build_recall_context()`
`state_machine.init_state(...)``session_persistence.create_session(...)`. DB가 없으면
- `POST /sessions``get_catalog_persona(code)`로 승인 카드 조회 → `case_profile` upsert →
직전 `session_summary` seed recall → `state_machine.init_state(...)`
`session_persistence.create_session(...)`. DB가 없으면
`runtime_fallback_allowed()` 확인 후 `store.create(...)`로 in-proc 생성.
- `POST /sessions/{id}/turn` — 동기 경로. `prepare_turn``run_turn_generate`
학습자 발화 turn(speaker=`counselor`, 원문 `text` + `text_masked`) + 내담자 응답 turn(speaker=`client`)
@ -414,7 +417,7 @@ DB는 PostgreSQL 16 + pgvector(단일 SoR). 초기화 SQL은 `infra/db/init/`에
evaluator 컨텍스트로 fast-loop 평가 rows를 적재한다. 리뷰 경로만 evaluator 컨텍스트로 hydrate한다.
- 종단: `app.learner_profile`(dim_ewma/dim_slope/persistent_gaps — 회기 거듭하며 나아지는지).
- 감사(append-only): `audit.persona_drift_log`(임베딩 일관성/CCD 누출), `audit.audit_log`(인간 열람 추적),
`audit.llm_call_log`(비용·inference_geo).
`audit.llm_call_log`(비용·inference_geo·latency metadata-only; prompt/completion 본문 미저장).
### 5.3 ds 스키마 — 재귀학습 파이프라인 (`infra/db/init/04_audit_eval_rls.sql` §4)
@ -438,7 +441,28 @@ DB 레벨 이중강제(`04_audit_eval_rls.sql` §5, `app/db.py` `acquire()`):
---
## 6. degraded / 폴백 정책
## 6. 인증/OAuth 경계
- 브라우저는 BFF API에만 로그인한다. Google OAuth는 Authorization Code + PKCE를 사용하고, 완료 후
서버가 opaque HttpOnly 쿠키(`__Host-vignette_sid`)를 발급한다. id/access token은 브라우저에 저장하지 않는다.
- OAuth `state`는 서버 메모리 `_oauth_states`에 호환용으로 보관하지만, 동시에 HMAC 서명 토큰 자체로
`next`, 발급시각, nonce를 검증하고 PKCE verifier를 재계산한다. 시작 응답은 같은 state를 HttpOnly
`__Host-vignette_oauth_state` 쿠키로도 내려주며, callback은 URL state와 쿠키 state가 일치할 때만 복구한다.
그래서 public API가 로그인 시작과 콜백 사이에 재시작돼도 callback은 `invalid_state`로 실패하지 않고
Google token exchange 단계까지 가며, 쿠키 없는 외부 callback 주입은 차단된다.
- OAuth callback 실패는 authorization code, token, raw email을 남기지 않고 reason/status/도메인 수준 정보만
서버 로그에 남긴다. 프론트는 `token_exchange_failed`, `invalid_state`, provider error(`access_denied`/`provider_error`),
identity claim 실패를 구분하고 실패 reason code를 화면에 함께 표시한다.
- 역할은 `AUTH_TEACHER_EMAILS`/`AUTH_ADMIN_EMAILS` email allowlist로 1차 판정한다. 코호트는
`AUTH_EMAIL_COHORT_MAP``AUTH_DOMAIN_COHORT_MAP` 설정, SAML fixture의 `cohort` claim을 합쳐
`cohort_ids`로 세션에 저장한다. 관리 사용자 `app_user.external_id`는 provider subject 기반
(`google:`/`saml:`/`dev:`)으로 저장해 email 변경 리스크를 줄인다.
- 로컬/Tailnet dev는 dev-login을 사용한다. 공개 `OAUTH_REDIRECT_URI`가 로컬/Tailnet 세션이 아니라 public API
세션으로 돌아가는 혼선을 막기 위해 dev-origin의 Google 직접 시작은 `local_oauth_unavailable`로 차단한다.
---
## 7. degraded / 폴백 정책
| 미가용 대상 | 증상 | 동작 |
|---|---|---|
@ -452,7 +476,7 @@ DB 레벨 이중강제(`04_audit_eval_rls.sql` §5, `app/db.py` `acquire()`):
---
## 7. 로컬 실행 (Windows 11 + PowerShell 기준)
## 8. 로컬 실행 (Windows 11 + PowerShell 기준)
> 한글/공백 경로 주의. `apps/api/.env`는 pydantic-settings가 자동 로드한다.
@ -510,7 +534,7 @@ npm run e2e # Playwright (web+api+DB 스택 필요)
---
## 8. 안전 불변식 요약(설계 보증)
## 9. 안전 불변식 요약(설계 보증)
- 마스킹되지 않은 원문은 외부 LLM 경로로 절대 나가지 않는다(입력 가드레일 하드 게이트, F-03).
- 내담자 AI는 CCD·진단 차원·정답 라벨·상태 수치를 *말로 설명하지 않는다*(L0 + 출력 가드레일 이중방어, R4).

View file

@ -20,18 +20,21 @@ DB가 없으면 인메모리 degraded 폴백으로 기동한다.
## 0. 가장 빠른 경로 — 한 커맨드 (권장)
게이트웨이(9099)+API(8000)+웹(5173)을 한 번에 깔끔히 (재)기동한다. 기존/고아 프로세스를
커맨드라인 기준으로 정리한 뒤 결정론적으로 띄운다(`--reload` 워처 불안정 회피).
게이트웨이(9099)+API(8000)+웹(5173)을 한 번에 깔끔히 (재)기동한다. Docker가 사용 가능하면
`vignette-dev-db` Postgres(pgvector) 컨테이너도 보장하고 health/role을 점검한다. 기존/고아
프로세스는 커맨드라인 기준으로 정리한 뒤 결정론적으로 띄운다(`--reload` 워처 불안정 회피).
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\dev-up.ps1
# 종료: powershell -NoProfile -ExecutionPolicy Bypass -File scripts\dev-down.ps1
# DB 컨테이너까지 멈출 때만: powershell -NoProfile -ExecutionPolicy Bypass -File scripts\dev-down.ps1 -Db
```
- 진입점 **http://localhost:5173** → 로그인 페이지에서 **dev-login**(아무 `@hs.ac.kr`, role learner/teacher/admin).
- DB 없이 in-memory degraded로 뜨고, seed 페르소나(P1~P3)·AI 턴 생성까지 동작.
- Docker가 있으면 기본적으로 `127.0.0.1:55432` DB 컨테이너를 사용한다. 새 컨테이너 생성 시 `POSTGRES_USER=vignette_owner`, API용 앱 role은 `DATABASE_URL` 사용자로 분리해 RLS 검증 기반을 보존한다. Docker가 없거나 `-NoDb`를 쓰면 in-memory degraded로 뜬다.
- 로그는 `.devlogs/`(gitignore). 코드 수정 후에는 dev-up을 다시 실행해 재기동(reload 미사용).
- 옵션: `-NoGateway`(UI만), `-NoWeb`(API만).
- 옵션: `-NoGateway`(UI만), `-NoWeb`(API만), `-NoDb`(DB 컨테이너 보장 건너뜀).
- `dev-down.ps1`은 기본적으로 DB 컨테이너를 보존한다. 컨테이너도 멈추려면 `-Db`를 명시한다.
> 스크립트는 uvicorn이 설치된 python을 자동 해석한다(시스템에 복수 python 공존 시 'python' 별칭이
> uvicorn 없는 인터프리터를 가리킬 수 있음 — 이 함정 때문에 명시 해석함).
@ -48,6 +51,7 @@ powershell -NoProfile -ExecutionPolicy Bypass -File scripts\start-tailscale-runt
- 이 경로는 `tailscale serve``https://alpaca-home.taile93291.ts.net` → 로컬 web `127.0.0.1:5173`을 연결한다.
- API는 Tailnet 전용으로 `127.0.0.1:8010`에 뜨고, Vite proxy는 `VITE_API_PROXY_TARGET`으로 이 포트를 본다. 기존 8000 reloader 잔류와 충돌하지 않기 위해 분리했다.
- dev-login은 `AUTH_DEV_LOGIN_EXTRA_ORIGINS`에 Tailnet origin이 들어간 경우에만 dev 환경에서 열린다. prod에서는 열리지 않는다.
- Tailnet/로컬 dev에서는 Google OAuth를 사용하지 않는다. OAuth redirect URI가 공개 API callback으로 고정된 동안에는 콜백이 로컬/Tailnet 세션이 아니라 공개 API 세션으로 돌아가므로, 로그인 화면은 Google 버튼을 비활성화하고 직접 `/api/auth/login?provider=google`을 열어도 `local_oauth_unavailable` 안내로 되돌린다.
### 수동 기동(대안)
@ -112,6 +116,8 @@ npm install
| `ENGINE_MODE` | `claude_cli` | 엔진 provider 라우팅 |
| `AUTH_DEV_LOGIN_ENABLED` | `true` | dev-login 엔드포인트 활성화 |
| `AUTH_ALLOWED_EMAIL_DOMAINS` | `["hs.ac.kr","twentyoz.kr"]` | 로그인 허용 이메일 도메인(dev-login 포함 검증) |
| `AUTH_EMAIL_COHORT_MAP` | `{}` | 특정 이메일을 cohort id로 매핑한다. 값은 comma-separated 문자열도 허용 |
| `AUTH_DOMAIN_COHORT_MAP` | `{}` | 이메일/Google hosted domain을 cohort id로 매핑한다. Google/SAML/dev-login 세션 `cohort_ids`에 반영 |
| `VIGNETTE_VOICE_POC_SAMPLE_TTS` | `false` | P1 무참조 샘플 음성을 `/voice/ws` TTS에 연결하는 개발 전용 플래그. 마이크/STT는 `OPENAI_API_KEY` 필요, 프로덕션 금지 |
> 참고: 프로세스 환경변수(`$env:KEY`)는 `.env`보다 우선한다. 일회성 오버라이드에 쓸 수 있다.
@ -222,6 +228,9 @@ curl.exe http://127.0.0.1:8000/auth/me -b cookies.txt
`apps/web`의 로그인 화면(`Login.tsx`)에 dev-login 경로가 있다. 웹을 띄운 상태(5173)에서
프록시를 통해 `/api/auth/dev-login`으로 동일하게 동작한다.
> 로컬/Tailnet 테스트는 dev-login을 사용한다. Google OAuth는 공개 도메인
> `https://vignette.chanpaca.net`에서만 실제 계정 흐름으로 검증한다.
---
## 4. 웹(프런트엔드) 실행
@ -320,7 +329,7 @@ docker compose up -d # 전체
```powershell
# 백엔드 (apps/api)
cd apps\api
python -m pytest app\ -q # 현재 98 pass
python -m pytest app\ -q # 현재 113 pass
python -m pytest engine_gateway\ -q # 7 pass
# 웹 (apps/web)

View file

@ -45,34 +45,34 @@
## 2. 갭 로드맵 — 심각도 우선순위
심각도 분류: **critical 3 / high 4 / medium+ 6**. `✓`는 작성자가 코드 grep으로 직접 재확인한 항목, `(분석)`은 분석 결과로 착수 전 코드 1차 재확인 권고.
심각도 분류: **critical 3 / high 4 / medium+ 6**. C1의 "구조 전무"는 1차로 해소됐고, 편집·저장·임상 루브릭·AI 채점은 계속 추적한다. `✓`는 작성자가 코드 grep으로 직접 재확인한 항목, `(분석)`은 분석 결과로 착수 전 코드 1차 재확인 권고.
### Critical (3)
| ID | 갭 | 현재상태 | 권고 | 근거 |
|---|---|---|---|---|
| **C1** | 사례개념화·치료계획 산출물 구조 전면 부재 | `cognitive_triad/case_conceptual/인지삼제/4사분면/quadrant` grep **0건** ✓. CCD는 숨은 정답키일 뿐 학습자 산출물 폼이 아님. | 워크시트 스키마(11탐색항목·호소 5영역·인지삼제·1·2차감정·보호/방해 4사분면·생물심리사회 목표) 입력 폼 + AI 축어록 초안 추출 + 규칙 기반 채점. 루브릭은 임상팀 외부 정의 가능하게 외부화. | doc1·doc4·doc5 |
| **C2** | 위기개입 프로토콜·생명유지서약·에스컬레이션 미구현 | `escalate=True` 시 client stream 이벤트(`StreamEvent("safety")`)만, `safety_events` DB 적재·교수자 알림 코드 **없음** ✓. `prepare_turn`이 risk_level을 상태머신 `ideation_observed`로 미전달. | 위기 분기 상태(예외고지→단계적 탐색→서약)를 상태머신에 추가, escalate 시 safety_events insert+교수자 알림, 위기탐색 누락 시 회기리뷰 감점, ideation_observed 전달. | doc1·doc5(핵심 시나리오)·doc4(IRB 전제) |
| **C3** | 이론모드 미주입 + CBT 콘텐츠 자체 부재 | `build_turn_messages` 시그니처에 `theory` 인자 **없음** + `apps/web/src/pages/Session.tsx:589` `sessionApi.start(personaCode, "humanistic")` 하드코딩 ✓. CBT 프롬프트 체인·이론부합 루브릭 전무. | theory 인자 + 이론별 단계 프롬프트 체인(공감·반영 / 인지재구조화·행동활성화) + 프론트 이론 선택 UI + 이론 부합도 루브릭. | doc4(humanistic+CBT 필수)·doc2/doc5 |
| **C1** | 사례개념화·치료계획 산출물 구조 1차 구현·고도화 필요 | `SessionReviewResponse.caseWorksheet`와 리뷰 화면 read-only 카드가 축어록 근거 기반 초안을 제공한다. CCD는 숨은 정답키이고, 현재 워크시트는 저장형 학습자 제출물은 아님. | 편집·DB 저장형 워크시트, AI 축어록 초안 추출 고도화, 규칙 기반 채점, 교수자 검수 플로우. 루브릭은 임상팀 외부 정의 가능하게 외부화. | doc1·doc4·doc5 |
| **C2** | 위기개입 프로토콜·생명유지서약·에스컬레이션 1차 배선 완료·임상 고도화 필요 | 실제 자해·자살 신호는 LLM/엔진 호출 전 중단하고 109 리소스와 `conversation_stopped`를 REST/SSE/voice 응답에 싣는다. `crisis.risk_level`은 상태머신 `ideation_observed`로 전달하고, escalate 시 `app.safety_events` detail 적재 및 교수자 대시보드 안전 알림 큐로 연결한다. | 남은 것은 임상팀 콘텐츠: 비밀보장 예외고지→단계적 탐색→생명유지서약 스크립트, 실시간 push/메일 알림 정책, 위기탐색 누락 시 회기리뷰 감점 루브릭. | doc1·doc5(핵심 시나리오)·doc4(IRB 전제) |
| **C3** | 이론모드 1차 배선 완료·CBT 콘텐츠/선택 UI 잔여 | `TurnContext`/`prepare_turn`/sessions/voice/evaluator에 `theory_mode`가 전달되고, `build_turn_messages`도 인간중심·CBT·통합 이론 프레이밍을 엔진 메시지에 넣는다. 프론트는 `persona.theory_target` 기준으로 시작해 기존 `humanistic` 하드코딩을 제거했다. | 임상팀이 확정한 CBT 단계 프롬프트 체인, 이론부합 채점 루브릭, 학습자/교수자용 명시적 이론 선택 UI. 현재는 페르소나 설계 이론 자동 선택이다. | doc4(humanistic+CBT 필수)·doc2/doc5 |
### High (4)
| ID | 갭 | 현재상태 | 권고 | 근거 |
|---|---|---|---|---|
| **H1** | 계약 평가 KPI(자기효능감·기술숙련도·수련만족도 사전사후) 수집·집계 전무 | `자기효능감/사전사후/수련만족/실험통제군` grep 0건. Phase3 KPI도 report shape만, 계산 코드 0줄. (분석) | 3척도 pre-post 폼·실험/통제군 배정·자동누적 대시보드·추이 시각화·검정 계산 코드. (κ/ICC·환각률은 doc4 미명시 → 평가설계 확정.) | doc4(20명 실험/통제군·단회기 50분·3척도 pre-post) |
| **H2** | 턴별 회기 리뷰(상호작용 분석 2열) 미가동 | `make_eval_hook``apps/api/app/services/evaluator.py:738`에 정의·export됐으나 `routes/`에서 주입 **0건** → 턴별 fast-loop 미동작(회기종료 deep-loop만) ✓. few-shot 골든셋 기본 OFF. | eval_hook을 turn 파이프라인에 주입. 회기리뷰 UI 좌(축어록 타임라인+비언어)/우(기법·적절성·대안반응·이론) 2열. 원천 축어록을 채점 few-shot 골든셋으로 적재. | doc2·doc5(골드 포맷) |
| **H2** | 턴별 회기 리뷰 fast-loop 1차 가동·골든셋/2열 UI 잔여 | `make_eval_hook`이 submit/voice 생성 경로에 주입되고, stream은 `_evaluate_stream_turn`으로 fast-loop 평가를 붙인다. 결과는 `feedback_scores`, `alternative_utterance` 등 정규화 테이블에 적재·hydrate된다. | 회기리뷰 UI 좌(축어록 타임라인+비언어)/우(기법·적절성·대안반응·이론) 2열 고도화, 원천 축어록 few-shot 골든셋 적재. | doc2·doc5(골드 포맷) |
| **H3** | 임상팀 콘텐츠 입력 경로(페르소나 저작 CRUD) 부재 + P4~P7 미적재 | personas 라우트에 검수 승인/반려만, draft 생성·편집 API 없음. `persona_repository.py`는 in-code `SEED_PERSONAS`(P1~P3)만 materialize, 외부 JSON 미로드 ✓. | 페르소나 저작 CRUD(draft→review) + P4~P7 적재, 루브릭·이론 콘텐츠를 임상팀 편집 가능 데이터로 외부화. | doc3(R&R)·doc4(페르소나=전문가 산출물) |
| **H4** | PII 마스킹 한국어 공백 + 외부전송 관측 부재 | Presidio `language='en'` 고정으로 한국어 이름/주소/기관 미탐지(폴백은 번호·이메일만). `audit.llm_call_log` 0행(런타임 미관측). `consent_at` 컬럼만, 동의 수집/게이트/철회 엔드포인트 전무. (분석) | 한국어 PII 탐지 추가, 마스킹 미들웨어 하드게이트화, llm_call_log 적재로 런타임 관측, 미성년/guardian 동의 수집·게이트·철회. | doc1/2/5(실명·날짜·미성년·자살시도 다수)·doc4(IRB·개인정보) |
| **H4** | PII 마스킹 한국어 공백 + 외부전송 관측 live proof 잔여 | Presidio `language='en'` 고정이라 이름/기관명은 NER 보강이 필요하지만, 한국어 날짜·금액·행정구역 주소 정규식 폴백은 추가됐다. 외부 LLM 호출은 상담 생성(generate/stream)·fast/deep 평가 직후 `audit.llm_call_log`에 provider/model/token/cost/inference_geo/latency만 적재하도록 연결했고, prompt/completion 본문은 저장하지 않는다. `consent_at` 컬럼만 있고 동의 수집/게이트/철회 엔드포인트는 아직 없다. | 한국어 이름/기관 NER 추가, 실제 운영 로그인 `/turn``audit.llm_call_log` row 실측, 미성년/guardian 동의 수집·게이트·철회. | doc1/2/5(실명·날짜·미성년·자살시도 다수)·doc4(IRB·개인정보) |
### Medium+ (6) — M1~M3, X1~X2, L1
| ID | 갭 | 현재상태 | 권고 |
|---|---|---|---|
| **M1** | 비언어/준언어 임상 이벤트 캡처·태깅 부재 | `voice.py`는 EOT용 `silence_ms`만, 침묵·한숨·울음 타임스탬프 이벤트 캡처·리뷰 표시 전무. 서버 RMS 힌트 프론트 미사용(dead). (분석) | 침묵·한숨·울음을 타임스탬프 메타 이벤트로 보존·시각화, '침묵 견디기'를 역량 지표화, 페르소나 의도적 침묵·비유창 한국어 렌더링. |
| **M2** | 다회기 종단 케이스 아크·교차회기 사례개념화 미구동 | `case_state.ccd_estimate/presenting_arc/alliance_level` 스키마만, `build_recall_context()` 빈 컨텍스트 반환. 음성 경로 빈 RecallContext. (분석) | case_state 런타임 구동, build_recall_context 실제 회상/pinned_fact 주입, 접수면접→다회기 연속성·자기개념 진화. |
| **M3** | SSO claim 매핑·식별자 안정성·deprovisioning 감사 미연결 | Google/SAML 콜백 `cohort_ids=[]` 하드코딩, role이 email allowlist, external_id가 `email:{}` 파생(불안정). SAML 서명검증 미구현. role변경/삭제 audit 미기록. (분석) | claim→role/cohort/institution_user_id 매핑+안정 식별자(sub/NameID), SAML 서명검증, deprovisioning audit, 한신 IdP 확정·외부 증거 수급. |
| **X1** | 재귀학습·데이터셋 export 파이프라인 미구현 | `ds.*` 스키마(κ/ICC 컬럼)만, read/write 코드 0건. JSONL export·IAA 게이트·골든셋 승격 미코딩. (분석) | JSONL export 잡, IAA 게이트(κ≥0.6/ICC≥0.75), 골든셋 승격. |
| **X2** | AI API 비용 관측 부재 | 게이트웨이 토큰 텔레메트리 0 고정, cost 모니터링·캐싱 부재. (분석) | 토큰 텔레메트리 실측, 저비용 모델 분기·캐싱, 비용 대시보드. |
| **M2** | 다회기 종단 케이스 아크·교차회기 사례개념화 부분 구동 | 1차로 `(persona_id, learner_id)` 안정 `case_profile` upsert, 원자적 `session_no`, 직전 `session_summary` 기반 seed recall, voice/REST/SSE recall cache 주입을 연결했다. `case_digest`/`pinned_fact` 실적재, episodic embedding writer, trajectory 갱신은 아직 없다. | case_state 런타임 보강, pinned_fact·case_digest 압축/갱신, 접수면접→다회기 연속성·자기개념 진화 실증. |
| **M3** | SSO claim 매핑·식별자 안정성 1차 완료·운영 IdP 감사 미연결 | Google/SAML/dev-login이 `AUTH_EMAIL_COHORT_MAP`·`AUTH_DOMAIN_COHORT_MAP` 및 SAML cohort claim을 `cohort_ids`로 전달하고, `app_user.external_id`는 provider subject(`google:`/`saml:`/`dev:`) 기반으로 저장한다. 운영 SAML 서명검증, 기관 claim schema/test tenant, deprovisioning audit은 아직 없다. | 한신 IdP 확정 후 SAML 서명검증, claim→role/cohort/institution_user_id 매핑 표 실연동, role변경/삭제 audit, deprovisioning evidence. |
| **X1** | 재귀학습·데이터셋 export 파이프라인 1차 구현 | `scripts/export-recursive-dataset.py``app.services.dataset_export`로 masked-text JSONL dry-run, PII scan, kappa/ICC 계산, approved export 게이트를 구현했다. 기본은 `technical_dry_run`이며 실제 승인 export·골든셋 승격은 데이터 steward/legal review와 IAA 통과가 필요하다. | 파일럿 evidence에서 reviewer disposition, steward/legal 승인, gold annotation 라운드 적재 후 `approved_for_recursive_learning_seed` 승격 검증. |
| **X2** | AI API 비용 관측·예산 경고 1차 완료·최적화 미구현 | 턴별 provider/model/tokens/cost 저장 경로와 `GET /admin/usage`, 관리자 비용 대시보드를 연결했다. `ADMIN_USAGE_BUDGET_USD` 기준 예산 상태(ok/warn/exceeded)도 응답/UI에 표시한다. DB 미가용 dev는 runtime store fallback, prod는 fail-closed다. 캐싱·저비용 모델 분기는 아직 없다. | 캐싱, 저비용 모델 분기, 장기 비용 추이/한도 정책. |
| **L1** | 기술스택 신청서-구현 불일치 및 단기일정 산출물 압박 | doc4 신청서 스택(Spring Boot 3/Node.js·TimescaleDB) vs 실제 FastAPI/Python 불일치, 20주 단기일정·9월 저작권 등재 압박. | 스택 정합 또는 변경 사유를 거버넌스 회의록으로, 9월 저작권 등재 문서화 수준을 일정 반영. |
> X2 근거: doc3 회의록이 'AI API 비용'을 운영 리스크로 명시.
@ -85,17 +85,18 @@
### A. 즉시 착수 가능 (내부 코드, 외부 합의 불요)
- **C2 배선**: escalate 시 `safety_events` insert + `ideation_observed` 전달.
- **C3 골격**: `theory` 인자 추가 + `Session.tsx:589` 하드코딩 제거 + 프론트 이론 선택 UI 골격.
- **H2 배선**: `make_eval_hook`을 turn 파이프라인에 주입(이미 정의·export됨, `evaluator.py:738`).
- **C1 1차 완료**: `caseWorksheet` 응답 구조와 리뷰 화면 read-only 워크시트 카드. 후속은 저장형 입력 폼·임상 루브릭·AI 추출/채점.
- **C2 1차 완료**: 위기 신호는 엔진 전 중단, 109 안내, `safety_events` 적재, 교수자 알림 큐, `ideation_observed` 전달까지 배선했다. 후속은 임상 스크립트·서약 문안·감점 루브릭.
- **C3 1차 완료**: `theory_mode`가 세션·평가·생성 프롬프트까지 흐른다. 후속은 임상팀 CBT 체인·이론부합 루브릭·명시적 선택 UI.
- **H2 1차 완료**: `make_eval_hook`과 stream 평가가 턴 파이프라인에 붙고 정규화 테이블로 적재·복원된다. 대안발화도 `app.alternative_utterance`로 정규화한다. 후속은 2열 리뷰 UI·골든셋.
- **H3**: 페르소나 저작 CRUD(draft→review) + P4~P7 로드 경로.
- **H4(부분)·M1(부분)·M2·X1·X2**: 마스킹 한국어 설정·하드게이트, 비언어 메타 이벤트 보존, `build_recall_context` 구현, `ds.*` read/write·export, 토큰 텔레메트리.
- **H4(부분)·X1·X2**: 마스킹 한국어 정규식 보강과 외부 LLM 호출 metadata-only `audit.llm_call_log` 적재 경로, dry-run JSONL export·PII scan·IAA 계산 1차는 완료했다. `ds.*` write는 `--write-dataset` 명시 시에만 수행한다. X2 비용 관측·예산 경고 1차는 완료했고 캐싱·저비용 모델 분기는 후속. M1/M2도 1차 구현 완료, provider 기반 비언어 감지와 case_digest/pinned_fact 실적재는 후속.
### B. 소유자 결정 / 외부(임상팀·기관) 의존
- **임상팀(구훈정·어유경) 산출물**: C1 워크시트 항목·채점 루브릭, C3 CBT 이론 콘텐츠·프롬프트 체인, C2 위기 스크립트·서약 문안, H2 골든셋 코딩. → 코드는 '편집 가능 구조'만 선제 구축.
- **임상팀(구훈정·어유경) 산출물**: C1 채점 루브릭·항목 확정, C3 CBT 이론 콘텐츠·프롬프트 체인, C2 위기 스크립트·서약 문안, H2 골든셋 코딩. → 코드는 구조를 선제 구축하되 임상 문안과 평가기준은 외부 정의로 받는다.
- **소유자 평가설계 결정**: H1 실험/통제군 배정·3척도 문항·50분 흐름. κ/ICC·환각률 목표는 doc4 미명시 → 평가설계 문서 확정.
- **기관(한신 IT) 의존**: M3 SSO IdP 프로토콜·test tenant·claim 스키마, SAML 인증서, 거버넌스 증거.
- **기관(한신 IT) 의존**: M3 SSO IdP 프로토콜·test tenant·claim 스키마, SAML 인증서, deprovisioning 거버넌스 증거.
- **거버넌스 결정**: L1 스택 정합성 처리 방향, 저작권 등재 문서화 수준, IP 협의.
---
@ -113,16 +114,16 @@
### 코드 재확인 grep 예시 (저장소 루트 `D:/workspace/vignette`)
```powershell
# C1: 사례개념화 구조 존재 여부 (현재 0건이어야 함)
rg -n "cognitive_triad|case_conceptual|인지삼제|4사분면|quadrant" apps
# C1: 사례개념화 워크시트 1차 구조와 잔여 저장/채점 경로 확인
rg -n "caseWorksheet|ReviewCaseWorksheet|사례개념화 워크시트|cognitive_triad|인지삼제|4사분면" apps
# C2: escalate 시 safety_events 적재 여부
rg -n "safety_events|escalate|ideation_observed" apps/api/app/services/orchestrator.py
# C3: 이론모드 하드코딩 위치
rg -n "humanistic" apps/web/src/pages/Session.tsx apps/api/app/services/persona.py
# C3: 이론모드 스레딩과 생성 프롬프트 주입 확인
rg -n "theory_mode|THEORY_MODE_GUIDANCE|sessionApi.start\\(personaCode" apps
# H2: eval_hook 정의 vs 주입
# H2: eval_hook 정의 주입
rg -n "make_eval_hook" apps/api/app
# H3: 페르소나 SEED(P1~P3) 한정 여부
@ -133,7 +134,7 @@ rg -n "SEED_PERSONAS" apps/api/app/persona_repository.py
```powershell
# 백엔드 (작업 디렉터리 apps/api)
python -m pytest app/ -q # 현재 77 pass
python -m pytest app/ -q # 현재 118 pass
python -m pytest engine_gateway/ -q # 현재 7 pass
# 프론트 (작업 디렉터리 apps/web)
@ -152,7 +153,7 @@ npm run e2e # Playwright — web+api+DB 스택 필요
- OCR 잔재·오탈자 존재(doc1·doc5). 의미는 시각 보정했으나 일부 표현 불확실.
- **doc4 κ/ICC·환각률은 신청서 본문 미명시** — 계약 확정 지표로 단정 금지.
- doc4/doc3 행정 불일치(참여교수 1명 vs 2명, 서식 연도 '2025' 오기, 연구책임자 표기 불일치, 트웬티온스 성명 공란).
- '현재상태'는 grep/코드 사실 기반 적대적 비평 인용. **C1·C2·C3·H2·H3은 작성자 직접 재확인(✓)**, 그 외(H1·H4·M1~M3·X1·X2·L1)는 착수 전 코드 1차 재확인 권고.
- '현재상태'는 grep/코드 사실 기반 적대적 비평 인용에서 시작했으나, **C1·C2·C3·H2·M3은 1차 구현 후 재검증 완료**, H3은 작성자 직접 재확인(✓) 기준이다. 그 외(H1·H4·M1·M2·X1·X2·L1)는 착수 전 코드 1차 재확인 권고.
---

View file

@ -15,8 +15,9 @@ Vignette 저장소의 모든 검증 수단(백엔드 단위 테스트, 웹 타
| 검증 | 작업 디렉터리 | 명령 | DB | API(8000) | 웹(5173) | 엔진GW(9099) | 브라우저 | 현재 통과 |
|---|---|---|---|---|---|---|---|---|
| 백엔드 단위 테스트 | `apps/api` | `python -m pytest app/ -q` | 불필요 | 불필요 | 불필요 | 불필요 | 불필요 | 98 pass |
| 백엔드 단위 테스트 | `apps/api` | `python -m pytest app/ -q` | 불필요 | 불필요 | 불필요 | 불필요 | 불필요 | 113 pass |
| 엔진 게이트웨이 테스트 | `apps/api` | `python -m pytest engine_gateway/ -q` | 불필요 | 불필요 | 불필요 | 불필요 | 불필요 | 7 pass |
| API 타입 생성 체크 | `apps/web` | `npm run check:api-types` | 불필요 | 불필요 | 불필요 | 불필요 | 불필요 | pass |
| 웹 타입체크 | `apps/web` | `npm run typecheck` | 불필요 | 불필요 | 불필요 | 불필요 | 불필요 | — |
| 웹 빌드 | `apps/web` | `npm run build` | 불필요 | 불필요 | 불필요 | 불필요 | 불필요 | — |
| Playwright E2E(전체) | `apps/web` | `npm run e2e` | **필요(+시드)** | **필요** | 자동기동 | 일부만 | **필요** | 102 tests / 17 files |
@ -41,14 +42,14 @@ Vignette 저장소의 모든 검증 수단(백엔드 단위 테스트, 웹 타
```sh
# apps/api
python -m pytest app/ -q # 앱 단위 테스트 (현재 98 pass)
python -m pytest app/ -q # 앱 단위 테스트 (현재 113 pass)
python -m pytest engine_gateway/ -q # 게이트웨이 단위 테스트 (현재 7 pass)
```
수집만 빠르게 확인하려면:
```sh
python -m pytest app/ --collect-only -q # → "98 tests collected"
python -m pytest app/ --collect-only -q # → "113 tests collected"
python -m pytest engine_gateway/ --collect-only -q # → "7 tests collected"
```
@ -84,6 +85,8 @@ python -m pytest engine_gateway/ --collect-only -q # → "7 tests collected"
- 작업 디렉터리: `apps/web`
- `package.json` 스크립트(실측):
- `generate:api-types` → FastAPI OpenAPI export 후 `src/lib/api.gen.ts` 재생성
- `check:api-types` → FastAPI OpenAPI export 후 생성 타입 stale 여부 확인
- `typecheck``tsc -b`
- `lint``tsc -b` (현재 lint는 타입체크와 동일)
- `build``tsc -b && vite build`
@ -96,10 +99,18 @@ python -m pytest engine_gateway/ --collect-only -q # → "7 tests collected"
```sh
# apps/web
npm install # 최초 1회 (devDependencies: typescript, vite, @playwright/test 등)
npm run check:api-types # FastAPI OpenAPI ↔ src/lib/api.gen.ts 동기화 확인
npm run typecheck # tsc -b — 타입 오류 0 확인
npm run build # tsc -b && vite build — 프로덕션 번들 생성까지 확인
```
API DTO를 바꿨다면 먼저 생성 산출물을 갱신한다.
```sh
# apps/web
npm run generate:api-types
```
---
## 3. Playwright E2E (풀스택)
@ -263,7 +274,7 @@ cd apps/web && npm run e2e
검증 결과는 **실제로 실행해 통과한 명령**만 근거로 보고한다.
- "통과했다/완료다"라고 말하려면 그 자리에서 **실행 가능한 검증 명령과 관측된 결과(통과 개수)**를
함께 제시한다. 예: `python -m pytest app/ -q``98 passed`.
함께 제시한다. 예: `python -m pytest app/ -q``100 passed`.
- 의존을 갖춘 검증을 우회하지 않는다. E2E를 DB/API 없이 돌려 놓고 "그린"이라고 보고하지
않는다. 풀스택을 못 띄웠으면 **"E2E 미실행"**이라고 명시한다.
- 인메모리 degraded 기동(`db:false`)에서 페르소나 헬퍼가 실패하는 것은 환경 문제이지 코드가

View file

@ -6,7 +6,7 @@
분류: **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 사례개념화 산출물·C2 위기개입 프로토콜·C3 이론모드/CBT가 신규 critical. 이는 아래 B1~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 사례개념화 산출물, C2 위기개입 프로토콜, C3 이론모드 스레딩은 1차 구조를 만들었다. 콘텐츠 정의는 임상팀(구훈정·어유경) 소유라 코드는 구조를 선제 구축하되 임상 문안과 평가기준은 외부 정의로 받는다.
---
@ -22,13 +22,13 @@
## B2. 환경 제약 — 이 워크스테이션에서 증거 생산 불가
- [ ] **`vnet.18ka.net` 공개 live 전환** — 코드/스크립트는 `vnet.18ka.net`·`api-vnet.18ka.net`을 허용하도록 갱신 완료. Tailnet 주소 `https://alpaca-home.taile93291.ts.net`로그인까지 검증됨. 공개 vnet은 현재 (1) `vnet.18ka.net`/`api-vnet.18ka.net` DNS 부재, (2) cloudflared 현재 인증 컨텍스트가 `18ka.net` zone이 아니라 `chanpaca.net` zone에 route dns를 생성, (3) prod API용 Postgres `127.0.0.1:55432` 미기동으로 8001 fail-closed, (4) Google Console redirect URI `https://api-vnet.18ka.net/auth/callback` 등록 필요 때문에 live 완료 아님. 상세: `docs/ops/tailscale-vnet-runtime-2026-06-27.md`.
- [ ] **`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`.
- [ ] **공개 Google OAuth 실제 `/turn` proof** — 로그인 가능한 계정으로 `storageState` 캡처 후 `E2E_PUBLIC_AUTH=1` + `chromium-public-auth` 1회 통과 필요. (소유자 지시로 이번엔 보류.)
- [ ] **음성 캐스케이드 live** — 실제 Deepgram STT / OpenAI TTS provider 키, 물리 마이크, 공개 WSS 50분 양방향 실측. provider 키/하드웨어 부재로 미실행(백엔드 계약·UI synthetic 경로는 단위/E2E로 증명됨).
- [ ] **claude_cli ↔ Messages API 폴백 동일성**`ANTHROPIC_API_KEY`가 있어야 Messages API 경로를 돌려 동일성 검증 가능. claude_cli 경로는 게이트웨이 probe로 live 실측 완료.
- [ ] **저항엔진 openness 곡선 DB 실증** — fast-loop 평가의 `client_state_read``app.turn_client_state` 적재 경로는 추가됨. 다만 dev DB 실데이터/다수 실턴(real LLM) 기반 openness 곡선 증거는 아직 필요. 현재는 `app.test_state_machine_resistance` 단위테스트와 `app.test_evaluation_persistence` 매핑 테스트로 1차 증명.
- [ ] **마스킹 게이트 운영 외부-send 관측** — `audit.llm_call_log` 0행(앱이 payload를 적재하지 않음). 엔진으로 나가는 실제 요청 payload를 캡처해 raw PII 부재를 관측하려면 로깅 경로 또는 프록시 캡처 필요. 현재는 `app.test_orchestrator_masking`로 generate/stream payload에 phone/email/RRN 부재 증명.
- [ ] **재부팅 후 watchdog smoke**`watch-public-runtime.ps1` + Scheduled Task가 재부팅 후 엔진/API/터널을 복구하고 public `/turn`이 통과하는지 실측. 재부팅 불가로 미실행(parser/ check-only 경로는 확인됨).
- [ ] **마스킹 게이트 운영 외부-send live proof** — 상담 생성(generate/stream)과 fast/deep 평가 직후 `audit.llm_call_log`에 provider/model/token/cost/inference_geo/latency만 적재하는 경로를 추가했다. prompt/completion 본문은 저장하지 않는다. 남은 증거는 실제 운영 로그인 `/turn` 후 live DB row를 확인하는 것이다. 현재는 `app.test_orchestrator_masking`로 generate/stream payload에 phone/email/RRN 부재와 감사 payload의 본문 미포함을 증명.
- [ ] **재부팅 후 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을 보강했다.
- [ ] **상주 엔진풀 RSS 실측** — probe로 TTFT/세션재사용/cost는 live 확인. 프로세스 RSS(메모리)는 별도 OS 계측 항목으로 남김.
---
@ -54,7 +54,7 @@
## Phase 3 파일럿 게이트 (실참여자 필요)
- [ ] 20명 교육용 파일럿 운영 / 효과성·KPI 측정(SUS·자기효능감·κ/ICC·환각률) / 재귀학습 데이터셋 1차 산출 / 개인정보·동의 감사. 문서/checker는 준비됨(`docs/phase3/*`, `scripts/check-phase3-artifacts.py`), 실제 evidence는 파일럿 시점.
- [ ] 20명 교육용 파일럿 운영 / 효과성·KPI 측정(SUS·자기효능감·κ/ICC·환각률) / 재귀학습 데이터셋 approved 산출 / 개인정보·동의 감사. 문서/checker와 dry-run exporter는 준비됨(`docs/phase3/*`, `scripts/check-phase3-artifacts.py`, `scripts/export-recursive-dataset.py`), 실제 approved evidence는 파일럿·steward/legal 검수·IAA 통과 시점.
---
@ -64,4 +64,17 @@
- live 운영 증거: engine config 운영값, 상주 엔진풀 probe(TTFT/cost/세션재사용), 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 키 필요). 프로덕션 탑재는 라이선스/권리 결정 전 금지.
- 부수: RLS smoke 스크립트 cleanup RLS 컨텍스트 버그 수정, `vignette_app`(NOBYPASSRLS) dev 역할 프로비저닝.
- 평가 SSOP 1차: `TurnRecord.evaluation``app.feedback_scores`/`turn_technique`/`turn_client_state`/`supervisor_comment`로 정규화 적재·복원. 원시 평가 테이블은 evaluator/admin RLS로 제한하고 서버가 evaluator 컨텍스트로 적재한다. `app.turns` insert는 `RETURNING id` 확인으로 silent success 제거. deep-loop `alternative_utterance` 정규화는 후속. **검증: `app.test_session_turn_persistence` + `app.test_evaluation_persistence` 14 tests OK.**
- 평가 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.**
- 계약 SSOT 1차: FastAPI OpenAPI를 `apps/web/src/lib/api.gen.ts`로 생성하는 `npm run generate:api-types`와 stale 체크용 `npm run check:api-types`를 추가했다. `TurnResponse`/`SessionEndResponse`는 생성 타입 alias를 사용해 기존 `crisis_kind`/`end_state` 드리프트를 닫았다. 나머지 수기 미러 DTO 전환과 CI 연결은 후속. **검증: `npm run check:api-types`, `npm run typecheck`, `pytest app/` 119 passed, `pytest engine_gateway/` 7 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`가 정규화된 턴 평가를 기반으로 학습자별 적절성·라포·기술 사용 추이를 집계하고, 교수 콘솔에 최근 회기/항목별 성장 카드와 안전 알림 큐를 함께 표시한다. **검증: `pytest app/test_teacher_dashboard.py app/test_session_turn_persistence.py app/test_voice_ws.py -q` 21 passed, `pytest app/` 119 passed, `npm run check:api-types`, `npm run typecheck`, `npm run build`.**
- 페르소나 저작 CRUD 1차: teacher/admin 전용 `POST/GET/PUT /personas/drafts` 경로를 추가해 draft 생성·조회·편집·검수요청을 열고, 교수 콘솔에 JSON 초안 작성/불러오기/수정 UI를 연결했다. 승인된 카드만 학습자 catalog에 노출하는 기존 경계는 유지한다. P4~P7 실제 임상 콘텐츠 입력은 후속. **검증: `pytest app/test_persona_review.py -q` 19 passed, `pytest app/` 119 passed, `npm run check:api-types`, `npm run typecheck`, `npm run build`.**
- M1 비언어 이벤트 1차: voice learner turn에 이미 저장하던 `audio_ref`/`silence_ms`/`speech_rate`/`barge_in`을 리뷰 API `nonverbal` 이벤트로 노출하고, 회기 리뷰 축어록에 침묵·발화 속도·끼어듦·음성 입력 칩을 표시했다. 한숨·울음 감지와 live 마이크/STT/TTS 실측은 후속. **검증: `pytest app/test_session_turn_persistence.py app/test_voice_ws.py -q` 17 passed, `npm run check:api-types`, `npm run typecheck`, `npm run build`, `session-review.spec.ts` 1 passed.**
- M2 다회기 케이스 아크 1차: DB 세션 생성 시 `(persona_id, learner_id)` 기준 `case_profile`을 upsert하고, `session_no`를 트랜잭션 안에서 원자 증가시키며, `InProcSession.case_id`가 매회 새 `runtime_case_id`가 아니라 안정 `case_id`를 가리키게 수정했다. 시작/submit/stream/voice 경로는 직전 `session_summary` 기반 seed recall을 사용한다. `case_digest`/`pinned_fact` 실적재, episodic embedding writer, trajectory 갱신은 후속. **검증: `pytest app/test_session_turn_persistence.py app/test_voice_ws.py -q` 18 passed, `pytest app/ -q` 119 passed, `npm run check:api-types`.**
- C1 사례개념화 워크시트 1차: `SessionReviewResponse.caseWorksheet`와 리뷰 화면 read-only 카드가 탐색 11항목·호소 5영역·인지삼제/감정·보호/방해·생물심리사회 목표 초안을 축어록 근거 기반으로 제공한다. 편집·DB 저장, 임상 루브릭, AI 추출/채점, 교수자 검수는 후속. **검증: `pytest app/test_session_turn_persistence.py -q` 15 passed, `pytest app/ -q` 119 passed, `npm run check:api-types`, `npm run typecheck`, `npm run build`, `session-review.spec.ts` 1 passed.**
- C3 이론모드 1차: `theory_mode``TurnContext`/sessions/voice/evaluator뿐 아니라 `build_turn_messages`의 엔진 메시지까지 전달된다. 프론트는 `persona.theory_target` 기준으로 시작해 `humanistic` 하드코딩을 제거했다. CBT 체인·이론부합 루브릭·명시적 선택 UI는 후속. **검증: `pytest app/test_orchestrator_masking.py app/test_session_turn_persistence.py -q` 20 passed, `pytest app/ -q` 119 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` 22 passed, `pytest app/ -q` 119 passed.**
- X2 예산 경고 1차: `ADMIN_USAGE_BUDGET_USD` 설정값을 기준으로 `GET /admin/usage``budget.status=disabled|ok|warn|exceeded`, 사용률, 잔여 예산을 반환하고 관리자 콘솔이 예산 상태 배너를 표시한다. 80% 이상 warn, 100% 이상 exceeded. 캐싱·저비용 모델 분기는 후속. **검증: `pytest app/test_runtime_policy.py -q` 20 passed, `pytest app/ -q` 119 passed, `npm run check:api-types`, `npm run typecheck`.**
- H4 LLM call audit 1차: 상담 생성(generate/stream)·fast-loop 평가·deep-loop 평가의 외부 LLM 호출 직후 `audit.llm_call_log`에 provider/model/token/cost/inference_geo/latency metadata만 적재한다. prompt/completion 본문은 저장하지 않고 감사 실패는 상담 루프를 막지 않는다. 실제 운영 `/turn` row proof는 B2에 남긴다. **검증: `pytest app/test_orchestrator_masking.py app/test_evaluation_persistence.py app/test_session_turn_persistence.py -q` 27 passed, `pytest app/ -q` 119 passed.**
- 공개/로컬/Tailnet 로그인 복구: public API 530 원인은 prod에서 개발 전용 `VIGNETTE_VOICE_POC_SAMPLE_TTS=true`가 fail-close된 것과 DB `app.admin_engine_config` 기본 행 부재였다. `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를 분리한다. **검증: `https://vignette.chanpaca.net/login` 200 + `index-BzQmv-jU.js`, `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`, local/Tailnet Google direct는 각 origin의 `/login?oauth=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 없음. 추가 확인 결과 기존 DB는 `POSTGRES_USER=vignette` 기반이고 API startup DDL이 owner 권한을 요구해 런타임 app-role 전환은 마이그레이션 owner/런타임 role 분리 후 진행해야 한다.**

View file

@ -36,6 +36,16 @@ The installer is idempotent. Re-running it updates the same task:
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\install-public-runtime-task.ps1 -IntervalMinutes 5
```
Do not add future domains to the default watchdog until DNS and Cloudflare
routing are live. For example, `api-vnet.18ka.net` is intentionally excluded
from the default checks while that domain has no DNS. After a public domain is
actually reachable, add it explicitly:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\install-public-runtime-task.ps1 `
-AdditionalPublicHealthUrls https://api-vnet.18ka.net/health
```
Task name:
```powershell
@ -62,6 +72,14 @@ Invoke-RestMethod http://127.0.0.1:8001/health
Invoke-RestMethod https://api-vignette.chanpaca.net/health
```
Optional extra public host check, only after the host resolves:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\watch-public-runtime.ps1 `
-CheckOnly `
-AdditionalPublicHealthUrls https://api-vnet.18ka.net/health
```
Optional real Claude CLI readiness smoke:
```powershell

View file

@ -16,13 +16,13 @@
| 주장 | 직접 검증 결과 |
|---|---|
| C1 사례개념화/인지삼제/4사분면 구조 부재 | `cognitive_triad/case_conceptual/인지삼제/4사분면/quadrant` grep **0건** ✓ |
| C2 위기 escalate가 safety_events 미적재 | `orchestrator.py:281` escalate 시 `StreamEvent("safety")`만 yield, DB insert·교수자 알림 **없음** ✓ (stream 신호는 존재) |
| C3 이론모드 미주입 | `services/persona.py:147 build_turn_messages` 시그니처에 `theory` 인자 **없음** + `Session.tsx:589` `"humanistic"` 하드코딩 ✓ |
| H2 턴별 평가 미가동 | `make_eval_hook` 정의·export(`evaluator.py:738`)됐으나 `routes/`에서 주입 **0건** ✓ |
| C1 사례개념화/인지삼제/4사분면 구조 | `SessionReviewResponse.caseWorksheet`와 리뷰 화면 read-only 카드 1차 구현. 편집·저장·임상 루브릭·AI 채점은 후속 ✓ |
| C2 위기 escalate 배선 | 실제 위기 신호는 엔진 호출 전 중단하고 109 리소스·`conversation_stopped`를 반환한다. `crisis.risk_level``ideation_observed`로 전달되고, escalate 시 `app.safety_events` detail 적재 및 교수자 대시보드 알림 큐로 연결된다. 안전 이벤트 DB insert payload는 회귀 테스트로 고정했다. |
| C3 이론모드 1차 배선 | `theory_mode`가 세션·평가·생성 프롬프트까지 전달되고 `Session.tsx` 하드코딩 제거 ✓ |
| H2 턴별 평가 1차 가동 | `make_eval_hook`이 submit/voice에 주입되고 stream 평가도 붙어 정규화 적재·복원 ✓ |
| H3 페르소나 저작 경로 부재 | `persona_repository.py`는 in-code `SEED_PERSONAS`(P1~P3)만 materialize, draft CRUD·외부 JSON 로드 **없음** ✓ |
그 외 항목(H1 KPI 수집, H4 한국어 PII, M1~M3, X1~X2, L1)은 분석 결과이며 착수 전 코드 재확인 권고. doc4 KPI 범위(κ/ICC·환각률)는 신청서 본문 미명시 — 별도 평가설계 문서 근거 가능성(과대해석 금지).
그 외 항목(H1 KPI 수집, H4 한국어 이름/기관 NER와 live proof, M1~M2, X1~X2, L1)은 분석 결과이며 착수 전 코드 재확인 권고. M3은 1차 구현 후 재검증 완료. doc4 KPI 범위(κ/ICC·환각률)는 신청서 본문 미명시 — 별도 평가설계 문서 근거 가능성(과대해석 금지).
---
@ -30,8 +30,8 @@
원천문서가 요구하는 다수 차원 대비 Vignette가 **가장 부족한 핵심 3가지**:
1. **임상 산출물 구조의 전면 부재** — doc1/doc4/doc5가 1급 산출물로 요구하는 사례개념화 워크시트(11탐색항목·호소 5영역·인지삼제·1·2차 감정·보호/방해 4사분면·생물심리사회 목표)가 코드·스키마에 전무.
2. **위기개입 안전 프로토콜의 미구현** — IRB 100% 준수 전제인 비밀보장 예외 고지·자살사고 단계적 탐색·생명유지서약·에스컬레이션이 없고, escalate=True여도 safety_events 적재·교수자 알림이 없음.
1. **임상 산출물 구조의 1차 구현과 고도화 필요** — doc1/doc4/doc5가 1급 산출물로 요구하는 사례개념화 워크시트(11탐색항목·호소 5영역·인지삼제·1·2차 감정·보호/방해 4사분면·생물심리사회 목표)는 리뷰 API와 read-only UI 초안으로 1차 구현됐다. 편집·DB 저장, 임상 루브릭, AI 추출/채점, 교수자 검수는 아직 후속이다.
2. **위기개입 안전 프로토콜의 1차 배선 완료와 임상 고도화 필요** — 실제 위기 신호는 LLM 호출 전 중단하고 109 안내·`safety_events`·교수자 대시보드 알림 큐로 연결한다. 남은 것은 비밀보장 예외 고지, 자살사고 단계적 탐색, 생명유지서약, 위기탐색 누락 감점 같은 임상 콘텐츠·루브릭이다.
3. **계약 평가 KPI 수집 체계의 부재** — doc4가 못박은 자기효능감·기술숙련도·수련만족도 3종 사전사후 비교와 20명 실험/통제군 설계가 코드에 0건.
## 2. 원천문서 5종 요지
@ -44,40 +44,40 @@
## 3. 부족한 부분 — 심각도 우선순위
### [critical] C1. 사례개념화·치료계획 산출물 구조 전면 부재
### [critical] C1. 사례개념화·치료계획 산출물 구조 1차 구현·고도화 필요
- **근거**: doc1, doc4(mockup '상담 설정·피드백 리포트'), doc5
- **현재상태**(✓직접검증): 사례개념화/cognitive_triad/4사분면 grep 0건. CCD는 숨겨진 정답키일 뿐 학습자 산출물 폼 아님.
- **권고**: 사례개념화 워크시트 스키마(11항목/5영역/인지삼제/4사분면/생물심리사회 목표)를 입력 폼으로, AI가 축어록에서 신념·감정·욕구 초안 추출·제안, 규칙 기반 채점. 루브릭은 임상팀이 외부 설정으로 정의 가능하게 외부화.
- **현재상태**(✓직접검증): `SessionReviewResponse.caseWorksheet`와 리뷰 화면 read-only 카드가 축어록 근거 기반 초안을 제공한다. CCD는 숨겨진 정답키이고, 현재 워크시트는 저장형 학습자 제출물은 아님.
- **권고**: 편집·DB 저장형 사례개념화 워크시트, AI 축어록 초안 추출 고도화, 규칙 기반 채점, 교수자 검수 플로우를 추가한다. 루브릭은 임상팀이 외부 설정으로 정의 가능하게 외부화.
### [critical] C2. 위기개입 프로토콜·생명유지서약·에스컬레이션 미구현
### [critical] C2. 위기개입 프로토콜·생명유지서약·에스컬레이션 1차 배선 완료·임상 고도화 필요
- **근거**: doc1, doc5(핵심 훈련 시나리오), doc4(IRB 전제)
- **현재상태**(✓직접검증): escalate=True 시 client stream 이벤트만, safety_events insert·교수자 알림 코드 없음. prepare_turn이 risk_level을 state_machine.evolve의 ideation_observed로 미전달.
- **권고**: (1) 위기 분기 상태(예외고지→단계적 탐색→서약 유도)를 상태머신에 추가, (2) escalate 시 safety_events insert + 교수자 알림, (3) 학습자 위기탐색 누락 시 회기리뷰 감점, (4) ideation_observed 전달.
- **현재상태**(✓직접검증): 실제 자해·자살 신호는 REST/SSE/voice에서 엔진 호출 전 중단하고 `crisis_resource(109)`, `conversation_stopped`를 반환한다. `prepare_turn`은 crisis risk level을 `ideation_observed`로 전달하고, `record_safety_event``app.safety_events`에 109 resource·turn_seq·matched detail을 적재한다. 교수자 대시보드는 `list_safety_alerts` 기반 안전 알림 큐를 노출한다.
- **권고**: 후속은 콘텐츠/정책 영역이다. 위기 분기 스크립트(비밀보장 예외고지→단계적 탐색→서약 유도), 실시간 push/메일 알림 정책, 학습자 위기탐색 누락 시 회기리뷰 감점 루브릭을 임상팀 산출물로 외부화한다.
### [critical] C3. 이론모드 미주입 + CBT 콘텐츠 자체 부재
### [critical] C3. 이론모드 1차 배선 완료 + CBT 콘텐츠/선택 UI 잔여
- **근거**: doc4(humanistic+CBT 필수), doc2/doc5
- **현재상태**(✓직접검증): build_turn_messages에 theory 인자 없음, Session.tsx:589 'humanistic' 하드코딩. CBT 프롬프트 체인·이론부합 루브릭 전무.
- **권고**: theory 인자 추가 + 이론별 단계적 프롬프트 체인(공감·반영 / 인지재구조화·행동활성화) + 프론트 이론 선택 UI + 이론 부합도 스코어링 루브릭.
- **현재상태**(✓직접검증): `TurnContext`/`prepare_turn`/sessions/voice/evaluator에 `theory_mode`가 전달되고, `build_turn_messages`도 인간중심·CBT·통합 이론 프레이밍을 엔진 메시지에 넣는다. 프론트는 `persona.theory_target` 기준으로 시작해 기존 `humanistic` 하드코딩을 제거했다.
- **권고**: 임상팀 확정 CBT 단계 프롬프트 체인(공감·반영 / 인지재구조화·행동활성화), 이론부합 스코어링 루브릭, 명시적 프론트 이론 선택 UI. 현재는 페르소나 설계 이론 자동 선택이다.
### [high] H1. 계약 평가 KPI(자기효능감·기술숙련도·수련만족도 사전사후) 수집·집계 전무
- **근거**: doc4(20명 실험/통제군·단회기 50분·3척도 pre-post)
- **현재상태**: 자기효능감/사전사후/수련만족/실험통제군 grep 0건. Phase3 KPI도 report shape만, 계산 코드 0줄.
- **권고**: 3척도 pre-post 폼, 실험/통제군 배정, 자동누적 대시보드, 추이 시각화, 검정 계산 코드. (κ/ICC·환각률은 doc4 미명시 — 평가설계 문서 확정 필요.)
### [high] H2. 턴별 회기 리뷰(상호작용 분석 2열) 미가동
### [high] H2. 턴별 회기 리뷰 fast-loop 1차 가동 + 골든셋/2열 UI 잔여
- **근거**: doc2, doc5(골드 포맷: 기법 태깅+적절성+대안반응+이론해석)
- **현재상태**(✓직접검증): make_eval_hook 정의·export됐으나 라우트 미주입 → 턴별 fast-loop 미동작(회기종료 deep-loop만). few-shot 골든셋 기본 OFF.
- **권고**: eval_hook을 turn 파이프라인에 주입. 회기리뷰 UI 좌(축어록 타임라인+비언어)/우(기법·적절성·대안반응·이론) 2열. 원천 축어록을 채점 few-shot 골든셋으로 적재.
- **현재상태**(✓직접검증): `make_eval_hook`이 submit/voice 생성 경로에 주입되고, stream은 `_evaluate_stream_turn`으로 fast-loop 평가를 붙인다. 결과는 `feedback_scores`, `alternative_utterance` 등 정규화 테이블에 적재·hydrate된다.
- **권고**: 회기리뷰 UI 좌(축어록 타임라인+비언어)/우(기법·적절성·대안반응·이론) 2열 고도화, 원천 축어록 few-shot 골든셋 적재.
### [high] H3. 임상팀 콘텐츠 입력 경로(페르소나 저작 CRUD) 부재 + P4~P7 미적재
- **근거**: doc3(R&R), doc4(페르소나=전문가 산출물)
- **현재상태**(✓직접검증): personas 라우트에 검수 승인/반려만, draft 생성·편집 API 없음. persona_repository는 in-code SEED(P1~P3)만, 외부 JSON 미로드.
- **권고**: 페르소나 저작 CRUD(draft→review)와 P4~P7 적재, 루브릭·이론 콘텐츠를 임상팀 편집 가능 데이터로 외부화.
### [high] H4. PII 마스킹 한국어 공백 + 외부전송 관측 부재
### [high] H4. PII 마스킹 한국어 공백 + 외부전송 live proof 잔여
- **근거**: doc1/2/5(실명·날짜·장소·금액·미성년·자살시도 다수), doc4(IRB·개인정보)
- **현재상태**(분석): Presidio language='en' 고정으로 한국어 이름/주소/기관 미탐지, 폴백은 번호·이메일만. audit.llm_call_log 0행(런타임 미관측). consent_at 컬럼만, 동의 수집/게이트/철회 엔드포인트 전무.
- **권고**: 한국어 PII 탐지 추가, 마스킹 미들웨어 하드게이트화, llm_call_log 적재로 런타임 관측, 미성년/guardian 동의 수집·하드게이트·철회.
- **현재상태**(재검증): Presidio language='en' 고정으로 이름/기관명은 NER 보강이 필요하다. 한국어 날짜·금액·행정구역 주소 정규식 폴백은 추가됐고, 상담 생성(generate/stream)·fast/deep 평가 호출 직후 `audit.llm_call_log`에 provider/model/token/cost/inference_geo/latency만 적재한다. prompt/completion 본문은 저장하지 않는다. consent_at 컬럼만, 동의 수집/게이트/철회 엔드포인트 전무.
- **권고**: 한국어 이름/기관 NER 추가, 실제 운영 로그인 `/turn``audit.llm_call_log` row 실측, 미성년/guardian 동의 수집·하드게이트·철회.
### [medium] M1. 비언어/준언어 임상 이벤트 캡처·태깅 부재
- **근거**: doc1/2/5(침묵 초·한숨·울음·떨림을 1급 단서)
@ -86,22 +86,22 @@
### [medium] M2. 다회기 종단 케이스 아크·교차회기 사례개념화 미구동
- **근거**: doc1(2~10회기), doc2(3회기 자기개념 진화)
- **현재상태**(분석): case_state.ccd_estimate/presenting_arc/alliance_level 스키마만, build_recall_context() 빈 컨텍스트 반환. 음성 경로 빈 RecallContext.
- **권고**: case_state 런타임 구동, build_recall_context 실제 회상/pinned_fact 주입, 접수면접→다회기 연속성·자기개념 진화.
- **현재상태**: 1차로 `(persona_id, learner_id)` 안정 `case_profile` upsert, 원자적 `session_no`, 직전 `session_summary` 기반 seed recall, REST/SSE/voice recall cache 주입을 연결했다. `case_digest`/`pinned_fact` 실적재, episodic embedding writer, trajectory 갱신은 아직 없다.
- **권고**: case_state 런타임 보강, build_recall_context의 pinned_fact·case_digest 실적재, 접수면접→다회기 연속성·자기개념 진화 실증.
### [medium] M3. SSO claim 매핑·식별자 안정성·deprovisioning 감사 미연결
### [medium] M3. SSO claim 매핑·식별자 안정성 1차 완료·운영 IdP 감사 미연결
- **근거**: doc3(교내 SSO), doc4(RBAC·익명화)
- **현재상태**(분석): Google/SAML 콜백 cohort_ids=[] 하드코딩, role이 email allowlist, external_id가 'email:{}' 파생(불안정). SAML 서명검증 미구현. role변경/삭제 audit 미기록. 거버넌스 증거(claim-map·동의 매트릭스) 부재.
- **권고**: SSO claim→role/cohort/institution_user_id 매핑+안정 식별자(sub/NameID), SAML 서명검증, deprovisioning audit, 한신 IdP 확정·외부 증거 수급.
- **현재상태**(✓직접검증): Google/SAML/dev-login이 `AUTH_EMAIL_COHORT_MAP`·`AUTH_DOMAIN_COHORT_MAP` 및 SAML cohort claim을 `cohort_ids`로 전달하고, `app_user.external_id`는 provider subject(`google:`/`saml:`/`dev:`) 기반으로 저장한다. 운영 SAML 서명검증, 기관 claim schema/test tenant, deprovisioning audit은 아직 없다.
- **권고**: 한신 IdP 확정 후 SAML 서명검증, claim→role/cohort/institution_user_id 매핑 표 실연동, role변경/삭제 audit, deprovisioning evidence.
### [medium/추적] X1. 재귀학습·데이터셋 export 파이프라인 구현
- **현재상태**(분석): ds.* 스키마(κ/ICC 컬럼)만, read/write 코드 0건. JSONL export·IAA 게이트·골든셋 승격 미코딩.
- **권고**: JSONL export 잡, IAA 게이트(κ≥0.6/ICC≥0.75), 골든셋 승격.
### [medium/추적] X1. 재귀학습·데이터셋 export 파이프라인 1차 구현
- **현재상태**: `scripts/export-recursive-dataset.py``app.services.dataset_export`로 masked-text JSONL dry-run, PII scan, kappa/ICC 계산, approved export 게이트를 구현했다. 기본 산출물은 `technical_dry_run`이며 실제 승인 export·골든셋 승격은 데이터 steward/legal review와 IAA 통과가 필요하다.
- **권고**: 파일럿 evidence에서 reviewer disposition, steward/legal 승인, gold annotation 라운드 적재 후 `approved_for_recursive_learning_seed` 승격 검증.
### [medium/추적] X2. AI API 비용 관측 부재
### [medium/추적] X2. AI API 비용 관측·예산 경고 1차 완료·최적화 미구현
- **근거**: doc3(회의록이 'AI API 비용'을 운영 리스크로 명시)
- **현재상태**(분석): 게이트웨이 토큰 텔레메트리 0 고정, cost 모니터링·캐싱 부재.
- **권고**: 토큰 텔레메트리 실측, 저비용 모델 분기·캐싱, 비용 대시보드.
- **현재상태**: 1차로 턴별 provider/model/tokens/cost 저장 경로와 `GET /admin/usage`, 관리자 비용 대시보드를 연결했다. `ADMIN_USAGE_BUDGET_USD` 기준 예산 상태(ok/warn/exceeded)도 응답/UI에 표시한다. DB 미가용 dev는 runtime store fallback, prod는 fail-closed다. 캐싱·저비용 모델 분기는 아직 없다.
- **권고**: 캐싱, 저비용 모델 분기, 장기 비용 추이/한도 정책.
### [low] L1. 기술스택 신청서-구현 불일치 및 단기일정 산출물 압박
- **근거**: doc4(Spring Boot 3/Node.js·TimescaleDB vs 실제 FastAPI/Python), doc3(20주)
@ -110,17 +110,18 @@
## 4. 즉시 착수 가능 vs 소유자 결정/외부 의존
### A. 즉시 착수 가능(코드/스키마 내부, 외부 합의 불요)
- C2(2),(4): escalate 시 safety_events insert, prepare_turn에서 ideation_observed 전달 — 배선 수정.
- C3: theory 인자 추가 + Session.tsx 하드코딩 제거 + 프론트 선택 UI 골격.
- H2: eval_hook을 turn 파이프라인에 주입(이미 정의·export됨) — 배선.
- C1 1차 완료: `caseWorksheet` 응답 구조와 리뷰 화면 read-only 워크시트 카드. 후속은 저장형 입력 폼·임상 루브릭·AI 추출/채점.
- C2 1차 완료: escalate 시 `safety_events` insert, prepare_turn의 `ideation_observed` 전달, 109 resource, 교수자 알림 큐까지 배선했다. 후속은 임상 스크립트·서약 문안·감점 루브릭.
- C3 1차 완료: `theory_mode`가 세션·평가·생성 프롬프트까지 흐른다. 후속은 임상팀 CBT 체인·이론부합 루브릭·명시적 선택 UI.
- H2 1차 완료: `make_eval_hook`과 stream 평가가 턴 파이프라인에 붙고 정규화 테이블로 적재·복원된다. 대안발화도 `app.alternative_utterance`로 정규화한다. 후속은 2열 리뷰 UI·골든셋.
- H3: 페르소나 저작 CRUD + P4~P7 로드 경로.
- H4(부분): Presidio 한국어 설정·룰, llm_call_log 적재, 마스킹 미들웨어 하드게이트.
- M1(부분), M2, X1, X2: 메타 이벤트 보존, build_recall_context 구현, ds.* read/write·export, 토큰 텔레메트리.
- H4(부분): 한국어 이름/기관 NER, 운영 로그인 `/turn` 후 llm_call_log row 실측, 동의 게이트.
- X1: dry-run JSONL export·PII scan·IAA 계산 1차 완료. `ds.*` write는 `--write-dataset` 명시 시에만 수행한다. X2 비용 관측·budget alert 1차는 완료했고 캐싱·저비용 모델 분기는 후속. M1/M2도 1차 구현 완료, provider 기반 비언어 감지와 case_digest/pinned_fact 실적재는 후속.
### B. 소유자 결정 / 외부(임상팀·기관) 의존
- **임상팀(구훈정·어유경) 산출물**: C1 워크시트 항목·채점 루브릭, C3 CBT 이론 콘텐츠·프롬프트 체인, C2 위기 스크립트·서약 문안, H2 골든셋 코딩 — 콘텐츠는 doc3 R&R상 임상팀 소유. 코드는 '편집 가능 구조'만 선제 구축.
- **임상팀(구훈정·어유경) 산출물**: C1 채점 루브릭·항목 확정, C3 CBT 이론 콘텐츠·프롬프트 체인, C2 위기 스크립트·서약 문안, H2 골든셋 코딩 — 콘텐츠는 doc3 R&R상 임상팀 소유. 코드는 구조를 선제 구축하되 임상 문안과 평가기준은 외부 정의로 받는다.
- **소유자 평가설계 결정**: H1 실험/통제군 배정·3척도 문항·50분 흐름. κ/ICC·환각률 목표는 doc4 미명시 → 평가설계 문서 확정.
- **기관(한신 IT) 의존**: M3 SSO IdP 프로토콜·test tenant·claim 스키마, SAML 인증서, 거버넌스 증거.
- **기관(한신 IT) 의존**: M3 SSO IdP 프로토콜·test tenant·claim 스키마, SAML 인증서, deprovisioning 거버넌스 증거.
- **거버넌스 결정**: L1 스택 정합성 처리 방향, 저작권 등재 문서화 수준, IP 협의.
## 5. 미해결·판독 한계 (quality_flags)
@ -129,4 +130,4 @@
- OCR 잔재(doc1 '괴게 씩씩'≈'되게 씩씩', doc5 따옴표 깨짐·오탈자). 의미는 시각 보정했으나 일부 표현 불확실.
- doc4 KPI 범위 주의: κ/ICC·환각률은 신청서 본문 미명시('정량 신뢰도 확보' 수준) — 계약 확정 지표로 단정 금지.
- doc4/doc3 행정 불일치: 참여교수 1명 vs 구훈정·어유경 2명 병기, 서식7 연도 '2025' 오기, 연구책임자 표기 불일치(류승택/구훈정), 트웬티온스 4인 성명 공란.
- '현재상태'는 grep/코드 사실 기반 적대적 비평 인용 — 위 검증 메모 5건은 작성자 직접 재확인. 그 외는 착수 전 코드 1차 재확인 권고.
- '현재상태'는 grep/코드 사실 기반 적대적 비평 인용에서 시작했으나, C1·C2·C3·H2·M3은 1차 구현 후 재검증 완료했다. H3은 작성자 직접 재확인 기준이며, 그 외는 착수 전 코드 1차 재확인 권고.

View file

@ -6,6 +6,8 @@
- 현재 머신에서 `tailscale serve` 루트 HTTPS가 `http://127.0.0.1:5173`으로 연결된다.
- Tailnet 런타임은 API를 `8010`에 띄우고, Vite proxy가 `VITE_API_PROXY_TARGET=http://127.0.0.1:8010`을 보도록 분리했다. 기존 8000 reloader 잔류와 충돌하지 않게 하기 위해서다.
- Tailnet dev-login은 `AUTH_DEV_LOGIN_EXTRA_ORIGINS=["https://alpaca-home.taile93291.ts.net"]`가 있을 때만 dev 환경에서 열린다. prod에서는 계속 차단된다.
- Tailnet dev-origin에서는 Google OAuth 버튼을 비활성화하고, 직접 `/api/auth/login?provider=google`을 열어도 `local_oauth_unavailable` 안내로 되돌린다. 현재 redirect URI가 `https://api-vignette.chanpaca.net/auth/callback`로 고정되어 있어 Tailnet API가 만든 OAuth state를 public API callback이 검증할 수 없기 때문이다. 원격 수동검증은 `로컬 테스트 계정으로 계속`을 사용한다.
- Vite proxy는 원래 프론트 `Host`/proto를 `X-Forwarded-Host`/`X-Forwarded-Proto`로 API에 넘긴다. 이 헤더가 없으면 dev OAuth 차단 redirect가 public 로그인으로 새는 회귀가 생긴다.
- `scripts/dev-up.ps1`는 API 포트와 `VITE_API_PROXY_TARGET`을 같은 값으로 강제한다. Vite가 8010을 보는데 API가 8000/8002로 떠서 `/api/auth/config`가 500을 내던 회귀를 막기 위한 조치다.
## 검증
@ -15,7 +17,10 @@
- `GET https://alpaca-home.taile93291.ts.net/login``200`
- `GET https://alpaca-home.taile93291.ts.net/api/auth/config``dev_login_enabled: true`
- `POST https://alpaca-home.taile93291.ts.net/api/auth/dev-login` + cookie 재사용 `GET /api/auth/me``200`
- `GET https://alpaca-home.taile93291.ts.net/api/auth/login?provider=google&next=%2Flearn``302 https://alpaca-home.taile93291.ts.net/login?oauth=local_oauth_unavailable`
- Playwright DOM 확인: `https://alpaca-home.taile93291.ts.net/login``로컬 테스트`, `학습자`, `교수자`, `관리자`, `로컬 테스트 계정으로 계속`이 표시된다.
- Tailnet auth readiness E2E: `PLAYWRIGHT_BASE_URL=https://alpaca-home.taile93291.ts.net PLAYWRIGHT_SKIP_WEB_SERVER=1 npx playwright test e2e/auth.spec.ts --project=chromium-desktop --grep "shows Google OAuth readiness"``1 passed`
- Tailnet dev-login E2E: `PLAYWRIGHT_BASE_URL=https://alpaca-home.taile93291.ts.net PLAYWRIGHT_SKIP_WEB_SERVER=1 npx playwright test e2e/auth.spec.ts --project=chromium-desktop --grep "shows Google OAuth readiness|logs in locally"``2 passed`
- 로컬 리스너:
- gateway `127.0.0.1:9099`
- Tailnet API `127.0.0.1:8010`
@ -36,7 +41,7 @@
- 현재 `vnet.18ka.net``api-vnet.18ka.net`은 DNS가 없다.
- 현재 cloudflared 인증 컨텍스트는 `18ka.net` zone을 관리하지 못한다. `cloudflared tunnel route dns vignette vnet.18ka.net` 실행 시 원하는 `18ka.net`이 아니라 `vnet.18ka.net.chanpaca.net` 레코드를 만들었다.
- prod API는 DB 연결이 없으면 fail-closed로 종료한다. 현재 `127.0.0.1:55432` Postgres가 꺼져 있어 `http://127.0.0.1:8001/health`가 뜨지 않는다.
- prod API는 현재 `127.0.0.1:8001`에서 `db:true`, `engine:true`로 건강하다. 다만 `api-vnet.18ka.net` DNS가 없어 외부 vnet 도메인으로는 아직 도달할 수 없다.
- Google OAuth Console에도 `https://api-vnet.18ka.net/auth/callback`을 Authorized redirect URI로 추가해야 실제 Google 로그인이 완료된다.
## 재기동 명령

View file

@ -140,8 +140,29 @@ Blocked fields:
## Operator Commands
Generate the export only through an approved read-only reporting job. Until that job is
implemented, use the manifest as a contract and mark exports `technical_dry_run`.
Generate the export through the read-only dry-run exporter unless the data steward has
approved writing `ds.*` rows. The default output is `technical_dry_run`; do not use
`approved_for_recursive_learning_seed` unless consent, withdrawal, PII, legal/privacy,
and IAA gates are all complete.
Read-only dry-run export:
```powershell
$dbUrl = (Get-Content apps\api\.env -Encoding UTF8 | Where-Object { $_ -match '^DATABASE_URL=' } | Select-Object -First 1) -replace '^DATABASE_URL=', ''
python scripts\export-recursive-dataset.py --database-url $dbUrl --output-root $root --limit 1000
```
Optional dataset table write for reviewer/gold workflow:
```powershell
python scripts\export-recursive-dataset.py --database-url $dbUrl --output-root $root --write-dataset
```
Approved export is blocked unless all gates are present:
```powershell
python scripts\export-recursive-dataset.py --database-url $dbUrl --output-root $root --export-status approved_for_recursive_learning_seed --allow-approved
```
Validate artifact presence and headers:
@ -172,4 +193,3 @@ Get-FileHash "$root\03-export\anonymized_dataset.jsonl" -Algorithm SHA256
- [ ] PII scan report is attached or referenced.
- [ ] Agreement metrics match `02-measures/kpi_report.json`.
- [ ] Export limitations state whether the file is gold data or a dry-run seed.