개선관리 요구사항과 Google 로그인을 완료

This commit is contained in:
Yun Chan 2026-08-28 16:07:09 +09:00
parent cc0a15b7c6
commit 2a39636163
112 changed files with 10166 additions and 527 deletions

View file

@ -10,6 +10,28 @@
---
## 개선관리 워크북 동기화 (2026-08-27)
> 이 표는 워크북 ID와 현행 구현을 잇는 얇은 추적표다. 내부 기술 DONE의 focused 증거는
> `guides/testing.md`, 상세 계약은 SSOT `dev_dashboard.html`이 소유한다. C-001은 기술 사전검증과 외부
> 임상 승인을 분리하며, 외부 증거가 없으므로 계속 열린 항목이다.
| ID | 상태 | 현행 경계 |
|---|---|---|
| C-001 | **OPEN · 외부 GATE** | 109·수단 상세 차단·ideation 상한·source pack 기술 사전검증은 완료. 임상 검토자 이름·소속 기관·검토일·서면 결정 전부가 있어야 닫는다. F절/B4에서 추적한다. |
| C-002 | **DONE · 내부 기술** | 첫 회기 라포 반영·한 초점 열린 질문 체크리스트, 근거 turn 연결, 이후 회기 not-applicable |
| C-003 | **DONE · 내부 기술** | 종료 회기 4건 미만 insufficient, 이후 최다 페르소나 비중 0.75 이상 훈련 집중 주의; 임상·공정성 판정 아님 |
| REQ-001 | **DONE · 내부 기술** | provider가 이메일을 검증한 모든 Google 계정은 도메인·사전등록 없이 learner·approved로 즉시 로그인. 관리자 exact-email 연구참여자 사전등록 create는 pending 고정, 승인은 별도 PATCH |
| REQ-002 | **DONE · 내부 기술** | admin protocol draft→active→retired, 라이선스 C/D 외부 LLM 금지, evaluator-only RAG·rollback; migration 17 owner-run/readiness-only |
| REQ-003 | **DONE · 내부 기술** | 페르소나 주호소가 JSON 키 순서나 surface 상태보다 우선 |
| REQ-004 | **DONE · 내부 기술** | learner 현재 설정 AND 세션 snapshot. OFF는 AI 파생 API 403/UI 무요청·혼합 결과 redaction, 사용자 입력·privacy 보존; teacher/admin 유지 |
| REQ-005 | **DONE · 내부 기술** | 새 회기는 승인 페르소나 `persona_id/version` 고정, 시작/상세 계약 노출, durable create 실패 503 |
| REQ-006 | **DONE · 내부 기술** | prompt와 구조화 결과의 상담자/내담자 identity를 `[COUNSELOR]`/`[CLIENT]`로 역할 마스킹 |
| REQ-007 | **DONE · 내부 기술** | 회기 종료를 평가보다 먼저 영속화해 옛 평가 실패 뒤에도 새 회기 생성·턴 진행 가능 |
| REQ-008 | **DONE · 내부 기술** | 기본 엔진+live-client readiness, 구조화 done 없는 SSE EOF는 `client_stream_incomplete`로 실패 |
---
## A. 배포·공개 런타임 게이트 [환경]
> 출처: `ops/backlog-2026-06-26.md` B2 · 대시보드 운영 탭 · `ops/tailscale-vnet-runtime-2026-06-27.md` · `ops/public-runtime-watchdog.md` · `HANDOFF.md`
@ -98,10 +120,14 @@
NAS `.env(0600)` 재구성 + api만 재생성), 클로드 예산 한도 재발으로 NAS `ENGINE_MODE=agy_cli` 전환. 잔여 권고:
공유기 DHCP 예약(호스트 MAC D8-BB-C1-A2-3C-5D), Docker VM 클럭 동기화(호스트 대비 ~4일 선행),
NAS 로컬 agy 이전(릴레이·노트북 의존 제거). 증거: `docs/ops/evidence/nas-relay-recovery-2026-08-18.json`.
- [ ] **stable-source task 재등록 + 재부팅 후 watchdog smoke** — 사용자 승인 clean commit의 detached release root에
watchdog·로그온 boot task를 함께 재등록하고 action의 commit/tree/script SHA pin, 5분 watchdog
`LastTaskResult=0`, 로그온 boot, 실제 Windows 재부팅 후 엔진/API/터널 자동 복구 + public `/turn`을 실측한다.
DNS 개통 후 `api-vnet.18ka.net``-AdditionalPublicHealthUrls`로 명시 추가.
- [ ] **stable-source task 재등록 + 재부팅 후 watchdog smoke** — 2026-08-27 stale AF_UNIX socket으로
Docker Desktop 백엔드와 공개 DB가 중단된 사건을 기존 container·volume 삭제/교체 없이 복구했다. 부모 IPC 폴더는
timestamp `.stale` 이름으로 보존했고, 기존 `vignette-dev-db``vignette_recovered_prod_20260807`만 재기동했다.
detached-clean `bf5f7352…0092`·tree `c39f917b…c88`에 watchdog·로그온 boot task를 재등록해
action root/commit/tree/script SHA pin, 5분 watchdog `LastTaskResult=0`·failcount `0`, local/public health
`status=ok·db=true·engine=true` 3/3을 확인했다. 남은 것은 실제 Windows 재부팅 후 엔진/API/터널 자동 복구와
authenticated public `/turn` 실측이다. DNS 개통 후 `api-vnet.18ka.net`
`-AdditionalPublicHealthUrls`로 명시 추가한다.
- [x] **노트북 로컬 음성 스택 STT 구현** — 2026-08-08 소유자 결정으로 음성 양쪽을 노트북 상주
로컬 모델로 쓴다(STT faster-whisper, TTS는 아래 항목대로 MeloTTS). 이전에는 코드에 로컬 STT 경로가 아예 없었고
(`voice_stt_provider``openai|deepgram` 둘뿐), interim/final 스트리밍 구현도 Deepgram 전용이었다.
@ -262,7 +288,11 @@
- [ ] C1 사례개념화 **확정 루브릭 콘텐츠** + AI 추출/채점 calibration (현재 scaffold_only).
- [ ] CBT 체인·이론부합 루브릭.
- [ ] 위기개입 프로토콜 임상 문안.
- [ ] **위기개입 프로토콜 임상 승인** — 기술 사전검토는 완료했다. P1 위기 분류, 실제 수련생 위기 시
엔진 전 차단·109 연결, 자살 수단 상세 차단, `ideation_stage <= 3`, 공식 source pack 계약을
`data/clinical/crisis-protocol-validation.json`
`docs/ops/clinical-crisis-protocol-review-2026-08-27.md`에 고정했다. 남은 것은 임상 검토자 이름·소속 기관·검토일·
서면 결정 네 값의 외부 승인이다. 이 증거 전에는 `clinical_status=approved`나 개선관리 시트 `완료`로 바꾸지 않는다.
- [ ] 평가 골든셋 콘텐츠.
## G. 운영 후속 [구현] (비차단)

View file

@ -300,10 +300,11 @@
<section class="pulse">
<div class="pulse-main">
<span class="eyebrow">project pulse</span>
<h1>지금 위치: <em>G0~G6 · G8 증거 DONE</em>. <em>NAS 엔진 복구 · G7 external proof</em> 남았다.</h1>
<p class="pulse-state"><b>현재:</b> 공개 앱과 current 주기 실회기는 GREEN, NAS만 health timeout이다. <b>다음:</b> 승인 relay → NAS 재검증 → G7 mic·human proof.</p>
<h1>지금 위치: <em>G0~G6 · G8 증거 DONE</em>. <em>G7 external proof · 재부팅 실증</em> 남았다.</h1>
<p class="pulse-state"><b>현재:</b> 공개 앱·API·DB와 current NAS 실회기는 GREEN이다. <b>다음:</b> 실제 Windows 재부팅 자동복구 → authenticated public 회기 → G7 mic·human proof.</p>
<details class="pulse-details">
<summary>운영 복구·watchdog·배포 상세 보기</summary>
<p class="pulse-state"><b>2026-08-27 Docker stale socket 인시던트·복구:</b> 공개 로그인 화면은 유지됐지만 API가 <code>db=false</code> degraded 뒤 502가 됐다. 원인은 Docker Desktop 4.82.0 백엔드가 이전 비정상 종료에서 남은 0바이트 AF_UNIX reparse socket <code>Docker\run\dockerInference</code><code>docker-secrets-engine\engine.sock</code>을 ERROR 1920으로 제거하지 못해 startup crash-loop한 결함이다. Docker 전체 종료 후 두 부모 IPC 디렉터리를 <code>run.stale-20260827-232006</code>·<code>docker-secrets-engine.stale-20260827-232006</code>으로 이동해 복구 가능하게 보존했고, container·image·volume 생성/삭제/교체 없이 기존 <code>vignette-dev-db</code><code>vignette_recovered_prod_20260807</code>만 시작했다. API는 detached-clean <code>bf5f7352…0092</code>·tree <code>c39f917b…c88</code>에서 routine API-only 복구했고 public health 3/3 <code>status=ok·db=true·engine=true</code>를 확인했다. 로그온·5분 watchdog도 같은 root/commit/tree와 exact boot/watchdog/start SHA에 재등록했으며 watchdog <code>LastTaskResult=0</code>·failcount <code>0</code>이다. 실제 공개 로그인 브라우저는 console error/warning 0이다. 실제 Windows 재부팅과 authenticated public <code>/turn</code>은 아직 별도 gate다.</p>
<p class="pulse-state"><b>2026-08-18 엔진 readiness 장애·수정:</b> 06:30~10:05 KST 공개 API가 <code>engine=false</code>로 약 3.5시간 degraded였다. 원인은 (1) <code>reasoning_effort=high</code> 콜드 프로브가 20초 타임아웃을 넘기면 실패가 성공과 같은 TTL(운영 1800초)로 캐시되는 게이트웨이 결함, (2) 공개 엔진 <code>claude -p</code>가 소유자 Claude 구독을 개발 에이전트 작업과 공유해 사용량 한도 소진 시 생성이 실패하는 운영 경계다. 워치독은 기본 <code>/ready</code>의 캐시 200만 보고 엔진을 healthy로 오판해 API만 재시작하는 루프에 빠졌다. 수정: 실패 전용 <code>ENGINE_READY_FAILURE_TTL_SECONDS</code>(기본 30초) 분리 + 프로브 타임아웃 20→45초, 회귀 4건 신설(게이트웨이 62/62·backend 986). detached-clean <code>4771b97c…c880</code>·tree <code>286723de…c1fa</code>로 API/게이트웨이/워치독·부트 task를 재승격·재등록했고 명시 실행 <code>LastTaskResult=0</code>, 공개 health ok·OpenAPI 126·local voice exact를 재확인했다. 1차 승격은 새 worktree에 <code>apps/api/.env</code>(gitignored 비밀값)가 없어 fail-closed로 실패했다 — 승격 절차에 env 이관 단계가 필요함을 기록한다(약 4분 다운타임).</p>
<p class="pulse-state"><b>2026-08-12 인시던트(NAS relay 복구 대기):</b> 공개 API는 health ok·db/engine true·OpenAPI 126·auth 401과 local voice를 유지한다. NAS 프리뷰는 앞선 3/3 <code>degraded·db=true·engine=false</code>에서 15:24 KST local health 3회 타임아웃으로 악화됐고 정적 로그인 화면만 계속 보인다. NAS API의 고정 <code>ENGINE_URL=http://192.168.0.223:9100</code> 대상 listener가 사라졌으며 공개 엔진은 별도 loopback <code>127.0.0.1:9099</code>에서 정상이다. G8 clean-head candidate 112/112·실제 origin 112/112와 rollback receipt는 완료 증거로 보존하지만, 현 NAS runtime은 relay를 source/secret/task에 결속해 복구하고 health 3/3·auth·OpenAPI·assets·실제 회기를 다시 통과할 때까지 DEGRADED다. 자동 재시작·포트 노출·배포는 실행하지 않았다. G7은 별도로 명시 동의 mic·동시 high-water·human evidence가 남는다.</p>
<p class="pulse-state">2026-08-07 공개 DB는 recovered named volume으로 전환했고 원본 container·dump·volume을 보존했다. 2026-08-12에는 휴면 Gradle daemon의 <code>8001</code> 점유·cloudflared/voice sidecar 중단과 PowerShell 5.1 native stderr 조기 종료 결함을 복구했다. DB mutation 전에 fresh custom dump <code>9cbda31e…0ce5</code>(7,532,625 bytes·TOC 1,492)를 고정했다. 공개 API·cloudflared는 detached-clean <code>a73bcd24…</code>·tree <code>b02b3a1b…</code>로 fresh 교체했고 public health 3/3·db/engine true·OpenAPI 126·local voice exact를 통과했다. 로그온·5분 watchdog 두 task도 같은 root/commit/tree/script SHA에 pin해 명시 실행 결과 0을 확인했다. 공개 runtime receipt는 <code>47388d58…9b08</code>, 무마이크 rehearsal evidence는 voice <code>61af2e98…</code>·runtime <code>c6e8670e…</code>·topology <code>89f1cb86…</code>이며 UUID/email literal 0이다. G7은 배포가 아니라 실제 mic·동시 high-water·human evidence만 남은 external GATE다.</p>
@ -692,6 +693,7 @@
<p class="dg-note">211차 복구·재검증(2026-08-18 15:05 자동화): 엔진 구독 한도 리셋 직후 NAS 엔진 릴레이 실제 1회 시작이 4종 SHA pin 그대로 <b>passed</b>(영수증 receipt-actual-20260818.json, PID 65328, ready 실생 통과). NAS→릴레이 TCP가 막혀 있던 것은 Python311 인바운드 허용 규칙이 Public 프로필뿐인데 이더넷이 Private이라서였고, LAN 서브넷 한정 9100 허용 규칙 추가(소유자 UAC 승인) 뒤 NAS origin health <b>3/3 연속 status=ok·db·engine true</b>, auth 401, OpenAPI 126을 실측 — 2026-08-12 이후 처음이다. current GREEN 표기는 actual-origin 실제 회기 turn→review smoke가 남아 보류. 같은 자동화의 직렬 게이트 재실행은 <b>31 passed/14 failed/12 did-not-run</b>로 실패: 파서 수정 후 첫 실행된 breakpoint 실측 스윕이 settings/learner 협폭 결함 91건을 보고했고(아침 실행에선 연쇄 중단으로 미실행), session-mvp·persistence·voice 실패군은 아침 동일 웹 코드 통과에 근거해 스택·데이터 드리프트가 의심된다 — 원인 조사로 이관, 가짜 증거 금지로 게이트 pending 유지. 증거는 <code>docs/ops/evidence/nas-relay-recovery-2026-08-18.json</code>·<code>usecase-tdd-2026-08-18.json</code>이다.</p>
<p class="dg-note">212차 핫픽스(2026-08-18 오후): 공개 <code>POST /sessions/{id}/end</code> 503(소유자 실사용 보고)의 원인은 회기 종료의 pinned_fact upsert가 <b>learner 역할 연결</b>로 실행된 것이었다. <code>app.pinned_fact</code> RLS는 learner에 INSERT(소유 케이스)만 허용하고 SELECT/UPDATE는 AI 컨텍스트·admin/instructor 전용이라 <code>SELECT … FOR UPDATE + INSERT … ON CONFLICT DO UPDATE</code> 조합이 충돌 후보 가시성에서 <code>InsufficientPrivilegeError</code>로 실패하고 fallback 차단이 503으로 노출됐다. 공개 DB 롤백 재현으로 단순 INSERT는 통과·ON CONFLICT 조합은 거부·<code>ai_view=evaluator</code> 컨텍스트는 전체 upsert 통과를 실증했고(<code>bf5f7352</code>), pinned_fact 저장만 같은 learner uid의 AI evaluator 뷰 연결로 분리했다. 검증: 관련 62 passed·app <b>924 passed</b>, 공개 8001 승격 후 health ok. 엔진이 건강해 deep 평가가 처음 팩트를 반환한 순간 발화한 잠복 결함으로, 아침 degraded 구간엔 팩트가 비어 이 경로가 실행되지 않았다.</p>
<p class="dg-note">213차 복구·전환(2026-08-18 심야): 대청소 사건(node_modules·구 런타임 워크트리·Temp 소실)과 Docker Desktop 사망·whisper 9882 WinNAT 포트 예약·DHCP LAN IP 회수(223→224)가 연쇄 발생했다. 릴레이를 현행 루트 <code>bf5f7352</code>에 같은 4종 핀으로 재기동(r3 영수증)하고 <code>192.168.0.224:9100</code>으로 바인딩(방화벽 규칙 재발행), NAS는 실행 컨테이너 env 전량을 <code>.env(0600)</code>로 재구성해 ENGINE_URL 224 적용 후 api만 재생성했다. 클로드 예산 한도(Reached maximum budget $0.5)가 재발해 NAS ENGINE_MODE를 <code>agy_cli</code>(gemini)로 전환 — 공개는 소유자가 이미 관리자 UI로 agy 전환 완료. 결과: NAS origin health <b>3/3 status=ok·db·engine true</b>, auth 401, OpenAPI 126, <b>actual-origin 실회기 turn→review smoke 통과</b>(agy 턴 2개→종료 200→리뷰 200) — 2026-08-12 이후 처음으로 201차 current GREEN 조건을 모두 충족했다. 회기 종료 503 핫픽스(<code>bf5f7352</code>)도 소유자 실세션 종료 성공으로 운영 검증. 권고: 공유기 DHCP 예약(호스트 MAC), Docker VM 클럭 동기화(호스트 대비 ~4일 선행 — DB 타임스탬프 오염), NAS 로컬 agy 이전. 증거는 <code>docs/ops/evidence/nas-relay-recovery-2026-08-18.json</code>이다.</p>
<p class="dg-note">214차 개선관리 워크북 동기화(2026-08-27): <b>C-002·C-003과 REQ-001~008은 내부 기술 구현 DONE</b>이다. 첫 회기 체크리스트와 종료 회기 기반 훈련 노출 지표를 추가했고, 외부 연구참여자 사전등록은 create에서 <code>pending</code> 고정 뒤 별도 PATCH 승인으로 분리했다. 프로토콜 레지스트리는 <code>draft→active→retired</code>, 라이선스 C/D의 외부 LLM 금지, evaluator-only RAG 색인·rollback을 강제하며 migration 17은 owner-run이고 startup은 readiness-only라 runtime DDL을 하지 않는다. 주호소는 surface보다 우선하고, 새 회기는 <code>persona_id/version</code>을 고정한다. 학습자 AI 피드백은 <b>현재 계정 설정 AND 세션 snapshot</b>일 때만 노출하며 OFF에서는 순수 파생 API 403·기존 share 404·혼합 응답 AI 필드 redaction·UI 파생 endpoint별 요청 0회를 적용한다. 축어록·저장 워크시트·outcome/calibration 입력·multimodal 동의/철회/삭제/raw audio/privacy 원장은 보존하고 alliance는 self-scores-only로 보여주며, privacy 삭제 조작면은 44px 이상·가로 overflow 0을 유지한다. teacher/admin 감독 뷰는 유지한다. 옛 회기 평가 실패는 degraded 리뷰로 남아도 새 회기를 막지 않는다. 헬스는 기본 엔진과 live-client를 함께 확인하고 구조화 <code>done</code> 없는 EOF는 <code>client_stream_incomplete</code>다. 상담자/내담자 이름은 prompt와 구조화 결과에서 <code>[COUNSELOR]</code>/<code>[CLIENT]</code>로 역할 마스킹한다. <b>C-001은 내부 안전 사전검증만 완료</b>했으며 임상 검토자 이름·소속 기관·검토일·서면 결정이 없어 외부 GATE로 계속 연다.</p>
<div class="dg-principles" aria-label="디자인 생성 가드레일">
<div><b>래스터만 사용</b><span>이미지 생성 도구 산출물은 PNG 기반 시안이다. SVG·벡터·와이어프레임·로고 시트로 해석하지 않는다.</span></div>
<div><b>기능 우선</b><span>메인 라우트의 실제 액션과 정보 구조를 먼저 반영한다. 장식은 기능을 가리지 않는 수준에서만 쓴다.</span></div>
@ -874,8 +876,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="doing" data-cat="인프라·스택·RAG" data-owner="0">
<button class="scard-head" aria-expanded="false"><span class="chip c-doing">DB RECOVERY · PUBLIC LIVE</span><span class="scard-mid"><span class="scard-title">공개 계정·회기 복구와 무손실 전환</span><span class="scard-sum">복구 named volume을 공개 55432에 전환했다. 데이터·auth·public health는 정상이고 task 자동복구는 stable-source 재등록을 기다린다.</span></span><span class="caret" aria-hidden="true"></span></button>
<div class="scard-body"><div class="kv k-good"><b>공개 전환</b><p>2026-08-07 18:26 KST owner 승인으로 <code>vignette_recovered_prod_20260807</code><code>vignette-dev-db:55432</code>에 연결했다. cutover 집계 users 84·sessions 30·turns 705, Google 계정 16·소유 회기 30, orphan 0이다. 2026-08-09 Docker Desktop 중단 후 동일 컨테이너만 재기동했고, fresh dump <code>f1fd569c…5f64</code>를 보존한 뒤 누락 migration 14만 단일 트랜잭션으로 적용했다. 현재 local/public health는 prod·DB true·engine true·claude_cli, OpenAPI 126, Google OAuth 302다. API/cloudflared와 watchdog·로그온 task는 detached-clean <code>a73bcd24…</code>·tree <code>b02b3a1b…</code>와 exact script SHA에 pin했고 두 task 명시 실행 결과 0·Ready를 확인했다. 실제 Windows 재부팅 smoke만 남아 있다.</p></div><div class="kv"><b>보존·rollback</b><p>전환 직전 DB는 <code>vignette-dev-db-pre-recovery-20260807-182642</code>로 중지 보존했고 exact dump SHA는 <code>6b84d5c8…8af1f</code>다. 원본 volume, post-merge dump <code>d24eaa75…2e32</code>, 최종 recovered dump <code>d7bcc396…68ed</code>, 2026-08-09 fresh dump <code>f1fd569c…5f64</code>, 자동 rollback을 실증한 실패 후보 2개도 삭제하지 않았다.</p></div><div class="kv k-warn"><b>사용자 확인</b><p>보안상 기존 <code>auth_session</code>은 복사하지 않아 모두 Google 재로그인이 필요하다. 계정·회기 원장은 복구돼 있다. 실제 사용자가 재로그인한 뒤 과거 소유 회기 목록이 보이는지 확인할 때까지 이 카드는 안정화 중으로 유지한다. 상세: <code>ops/public-db-recovery-rehearsal-2026-08-07.md</code>.</p></div></div>
<button class="scard-head" aria-expanded="false"><span class="chip c-doing">DB RECOVERY · PUBLIC LIVE</span><span class="scard-mid"><span class="scard-title">공개 계정·회기 복구와 무손실 전환</span><span class="scard-sum">복구 named volume을 공개 55432에 유지한다. 데이터·auth·public health와 stable-source task 자동복구는 정상이며 실제 재부팅 실증만 남았다.</span></span><span class="caret" aria-hidden="true"></span></button>
<div class="scard-body"><div class="kv k-good"><b>공개 전환</b><p>2026-08-07 18:26 KST owner 승인으로 <code>vignette_recovered_prod_20260807</code><code>vignette-dev-db:55432</code>에 연결했다. cutover 집계 users 84·sessions 30·turns 705, Google 계정 16·소유 회기 30, orphan 0이다. 2026-08-27 Docker Desktop stale AF_UNIX socket 사건에서도 container·volume을 교체하지 않고 동일 컨테이너만 재기동했다. 현재 local/public health는 prod·DB true·engine true·claude_cli다. API와 watchdog·로그온 task는 detached-clean <code>bf5f7352…0092</code>·tree <code>c39f917b…c88</code>와 exact script SHA에 pin했고 watchdog 명시 실행 결과 0·Ready·failcount 0을 확인했다. 실제 Windows 재부팅 smoke만 남아 있다.</p></div><div class="kv"><b>보존·rollback</b><p>전환 직전 DB는 <code>vignette-dev-db-pre-recovery-20260807-182642</code>로 중지 보존했고 exact dump SHA는 <code>6b84d5c8…8af1f</code>다. 원본 volume, post-merge dump <code>d24eaa75…2e32</code>, 최종 recovered dump <code>d7bcc396…68ed</code>, 2026-08-09 fresh dump <code>f1fd569c…5f64</code>, 자동 rollback을 실증한 실패 후보 2개도 삭제하지 않았다. 2026-08-27 문제의 IPC 부모 폴더 두 개도 timestamp <code>.stale</code> 이름으로 보존했다.</p></div><div class="kv k-warn"><b>사용자 확인</b><p>보안상 기존 <code>auth_session</code>은 복사하지 않아 모두 Google 재로그인이 필요하다. 계정·회기 원장은 복구돼 있다. 실제 사용자가 재로그인한 뒤 과거 소유 회기 목록이 보이는지 확인할 때까지 이 카드는 안정화 중으로 유지한다. 상세: <code>ops/public-db-recovery-rehearsal-2026-08-07.md</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">DEV STACK · DB 보존 패치</span><span class="scard-mid"><span class="scard-title">로컬 개발 스택 1-커맨드 기동 — <code>scripts/dev-up.ps1</code></span><span class="scard-sum">정지 DB는 start 우선, 신규 PGDATA는 고정 named volume, 일반 dev-up의 DB 삭제 경로는 0.</span></span><span class="caret" aria-hidden="true"></span></button>
@ -1102,7 +1104,7 @@
<tbody>
<tr><td>Web typecheck</td><td><code>npm run typecheck</code></td><td>Passed</td></tr>
<tr><td>Design SSOT / auth visual</td><td><code>npm run check:design-ssot</code> / <code>npx playwright test e2e/auth-visual.spec.ts --project=chromium-single-run --reporter=line</code> / <code>npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run --reporter=line</code></td><td>SSOT checker passed; login/onboarding light-dark desktop-mobile 1 passed; 14 core screens × 7 widths visual gate 14 passed.</td></tr>
<tr><td>Full Playwright E2E baseline</td><td><code>npm run e2e:parallel</code> / <code>npm run e2e:single-run</code> / <code>npm run e2e:list</code></td><td>2026-08-18 현재 수집은 <b>1111 tests / 61 files</b>(유스케이스 16테마 <code>uc-*.spec.ts</code> 239 시나리오 추가). 이 숫자는 수집량이며 현 작업트리 전체 GREEN과 동일하지 않다. G8 clean-head release gate는 candidate 112/112와 실제 NAS-origin 112/112를 통과했다. 이전 단일 120/120과 2026-07-15의 fixture desktop/mobile 166/166 + DB/engine/provider 직렬 49/49 = 215/215는 범위가 다른 역사 기준선으로 보존한다.</td></tr>
<tr><td>Full Playwright E2E baseline</td><td><code>npm run e2e:parallel</code> / <code>npm run e2e:single-run</code> / <code>npm run e2e:list</code></td><td>2026-08-27 현재 수집은 <b>1136 tests / 62 files</b>다. 이 숫자는 수집량이며 현 작업트리 전체 GREEN과 동일하지 않다. G8 clean-head release gate는 candidate 112/112와 실제 NAS-origin 112/112를 통과했다. 이전 단일 120/120과 2026-07-15의 fixture desktop/mobile 166/166 + DB/engine/provider 직렬 49/49 = 215/215는 범위가 다른 역사 기준선으로 보존한다.</td></tr>
<tr><td>Refactor governance P1~P8</td><td><code>ruff check app</code> / <code>pytest -q app</code> / <code>pytest -q engine_gateway</code> / <code>npm run typecheck</code> / <code>npm run check:api-types</code> / <code>npm run check:design-ssot</code> / <code>npm run check:dead-code</code> / <code>npm run check:duplication</code> / <code>npm run build</code> / <code>npm audit --audit-level=high</code> / full Playwright</td><td>Backend 400 passed, gateway 29 passed, web gates/build/audit passed, vulnerabilities 0, production duplication 1 clone/15 lines/0.03%, Playwright 215/215 passed. 상세 근거는 <code>ops/refactor-governance-2026-07-15.md</code>.</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>Outcome &amp; Alliance OS G0</td><td><code>py -3.11 -X utf8 -m pytest -p no:cacheprovider apps/api/app/test_measurement_contract.py apps/api/app/test_runtime_schema_ssot.py -q</code> / <code>scripts/check-measurement-ledger.sql</code> / measurement·API contract checks / web typecheck / DB-backed <code>session-persistence</code> focused E2E 3종</td><td>G0 contract/schema 11 passed, 기존 backend 100 passed, auth 39 passed. Python→JSON Schema→TypeScript→PostgreSQL enum·필수필드 계약이 일치하고 8개 deterministic benchmark가 검증됐다. Live PostgreSQL에서 learner/client/evaluator 가시 행 1/1/2, 교차 누수 0, append-only guard 2를 확인했다. 학습자 턴→교수자 대시보드, 워크시트 검수, 종료 deep 평가→durable 리뷰 E2E는 각각 1 passed. G0/AOS-001~004 완료.</td></tr>
@ -1112,8 +1114,10 @@
<tr><td>Outcome &amp; Alliance OS G4/G5 caller</td><td><code>pytest test_session_learning_producer.py test_deliberate_practice.py test_deliberate_practice_store.py test_calibration_transfer.py test_calibration_transfer_store.py</code> / <code>scripts/smoke-session-learning-producer.py</code></td><td>focused 100 passed, 전체 API 855 passed. 실제 종료 회기·durable turn 2개·ready 평가에서 production caller가 G4 처방 원장 5종을 각 1개 생성하고 deterministic replay한다. G5는 prediction lock 전 observation 0, lock callback 뒤 failed 독립 관찰과 evaluator model-run 각 1개이며 replay 중복 0이다. learner/teacher projection이 일치하고 mastery/pass/assessment/transfer 자동 승격은 전부 0건이다. 합성 교육 fixture이며 임상·실제 숙달 주장이 아니다.</td></tr>
<tr><td>학생 자기주도 전체 루프 UI</td><td><code>npx playwright test e2e/self-directed-learning-loop.spec.ts --project=chromium-desktop --project=chromium-mobile --workers=1 --reporter=dot</code> / <code>npx playwright test e2e/alliance-pulse.spec.ts --project=chromium-desktop --project=chromium-mobile --workers=2</code> / <code>npm run typecheck</code> / 고정 캡처 직접 QA</td><td>실제 <code>src</code> 홈 추천→목표 선택→텍스트/SSE 회기→종료·리뷰→G4 처방 키보드 CTA→새 회기 재연습 시작이 desktop/mobile 2/2, typecheck를 통과했다. 페이지 overflow 0, 홈 CTA 44px·4.5:1 이상 대비, typed launch intent와 replay 모드 보존, 내부 criterion/counterevidence/UUID 비노출을 확인했다. 390×844·320×568 활성 회기는 아바타/문구 겹침 0과 축어록/입력창 내부 포함을 통과했다. 320px 리뷰 1~5 척도는 다섯 선택지 44px 이상·내부/페이지 overflow 0이고 전체 desktop/mobile 8/8이다. 모든 미소유 API는 fixture 404이며 실제 로그인/API/DB 증거가 아니다.</td></tr>
<tr><td>Contract SSOT aggregate DTO</td><td><code>py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_evaluation_persistence.py app/test_evaluator_model_routing.py app/test_teacher_dashboard.py app/test_rbac_idor.py app/test_session_turn_persistence.py -q</code> / <code>npm run check:api-types</code> / <code>npm run typecheck</code> / <code>npm run build</code></td><td>50 backend passed; learner sessions, session review/worksheet, teacher dashboard, session start/detail DTOs use generated <code>ApiSchema</code> aliases with UI fallback. Stage responses are OpenAPI enum unions, including review phase key/label, reached phase, evaluation summary/trigger responses, and teacher session/growth stage. <code>.github/workflows/api-contract.yml</code> runs <code>npm run check:api-types</code> on API/Web contract changes.</td></tr>
<tr><td>개선관리 C-002·C-003 / REQ-001~008</td><td><code>pytest app/ -q</code> / <code>pytest engine_gateway/ -q</code> / 항목별 focused pytest·DB/browser E2E / <code>npm run check:api-types</code> / <code>npm run build</code></td><td><b>내부 기술 DONE.</b> 2026-08-28 최종 API 1002 passed, gateway 68 passed, Ruff·API type check·typecheck·lint·production build PASS. focused: C-002 3, C-003 11, REQ-001 auth 41 + callback access-log redaction 6 + OAuth UI desktop/mobile 4 + route-mock desktop/mobile 2 + 실제 관리자 DB/browser lifecycle 1, REQ-002·005 26(경고 1) + protocol 실제 DB lifecycle 1 + persona 실제 DB/browser lifecycle 1, REQ-003 34, REQ-004 직접 정책 85·관련 route/read-model 163·desktop/mobile 2, REQ-006 24, REQ-007 관련 163·live/session 50·DB E2E 1, REQ-008 health 1 passed. Google OIDC는 provider-verified 이메일을 도메인·사전등록 없이 learner·approved로 허용하고 suspended는 보존한다. 실제 관리자 E2E는 승인 전 403/대기→승인 후 동일 세션 200→피드백 OFF 영속 재조회→비활성화·세션 0을, persona E2E는 작성→승인→catalog→주호소→exact ID/version pin→controlled SSE→DB 2턴과 exact cleanup/설정 원복을 확인했다. 최종 레이아웃 재실행은 시각 게이트 15/15·포커스 106/106·세션 8/8, 실패·skip 0이며 격리 포트와 프로세스를 모두 종료했다. 전체 Playwright 현 작업트리 GREEN 수치로 환산하지 않는다.</td></tr>
<tr><td>개선관리 C-001 외부 임상 승인</td><td><code>data/clinical/crisis-protocol-validation.json</code> / <code>docs/ops/clinical-crisis-protocol-review-2026-08-27.md</code></td><td><b>외부 GATE · 미완료.</b> 109 연결, 자살 수단 상세 차단, <code>ideation_stage &lt;= 3</code>, 공식 source pack과 자동 안전 게이트는 기술 사전검증 완료다. 그러나 임상 검토자 이름·소속 기관·검토일·서면 결정 네 값이 모두 없으므로 <code>clinical_status=approved</code>나 개선관리 시트 완료로 바꾸지 않는다.</td></tr>
<tr><td>SEO/GEO share cards</td><td><code>pytest app/test_session_share.py app/test_session_turn_persistence.py -q</code> / <code>npm run generate:api-types</code> / <code>npm run typecheck</code></td><td>21 passed; session share creates hashed-token public unfurl payload without raw transcript, revoked token returns 404, OpenAPI generated share DTOs, review screen share button typechecks. Static <code>robots.txt</code>/<code>sitemap.xml</code>/<code>llms.txt</code> added.</td></tr>
<tr><td>Backend pytest baseline</td><td><code>pytest -q app</code></td><td>2026-07-31 전체 실행 439 passed. 외부 Starlette 의존성의 <code>python_multipart</code> PendingDeprecationWarning 1건만 존재한다.</td></tr>
<tr><td>Backend pytest baseline</td><td><code>pytest -q app</code></td><td>2026-08-28 전체 실행 1002 passed.</td></tr>
<tr><td>X2 evaluator routing/cache/cost trend</td><td><code>python -X utf8 -m pytest -p no:cacheprovider app/test_llm_pricing.py app/test_admin_ops.py app/test_usage_report.py engine_gateway/test_provider_registry.py engine_gateway/test_gateway_model.py -q</code> / API type generation/check / web typecheck/build / admin focused E2E·layout visual gate / authenticated public API smoke</td><td>최신 backend 439 passed, gateway 45 passed, desktop/mobile E2E 2 passed, 7폭 focused visual gate 1 passed. Claude CLI SDK 비용 추정값과 Agy/Gemini·Codex·Claude API 공식 참조단가를 분리하고, 기존 0달러 행의 조회 시 보정·단가 미등록 모델의 <code>미산정</code> 표시·예산 합산·리포트 경고까지 고정했다. Claude 토큰은 전체 agent tree와 캐시 입력을 포함한다. 과거 0/0 Claude 442건은 로컬 JSONL의 실제 usage와 유일 일치한 169건만 백필했고, 273건은 <code>token_unmetered_turns</code>로 남겼다. 인증된 공개 관리자 화면은 확인 시점 30일 Claude 183건 중 계량 125·미계량 58건을 표시했다.</td></tr>
<tr><td>H3 raw source isolation</td><td><code>python -B -m pytest -p no:cacheprovider app/test_persona_review.py app/test_live_coach_sources.py -q</code></td><td>38 passed; persona source registration records raw hash-only artifacts separately from sanitized evaluator-only RAG chunks, and <code>rag.index_document()</code> rejects <code>sensitivity=3</code>/raw-marker chunks before DB access.</td></tr>
<tr><td>H3 persona itemized authoring UI</td><td><code>npm run typecheck</code> / <code>npm run build</code> / <code>npm run check:api-types</code> / <code>npx playwright test e2e/teacher.spec.ts --project=chromium-single-run --workers=1</code> / <code>npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run --workers=1</code> / <code>npx playwright test e2e/session-layout.spec.ts --project=chromium-desktop --project=chromium-mobile --workers=1</code></td><td>Typecheck/build/API type drift check passed; teacher PersonaStudio focused 8 passed including structured list payload and labeled prompt preview without raw JSON; visual gate 9 passed with the prompt tab opened; session-layout desktop/mobile 8 passed.</td></tr>
@ -1190,16 +1194,16 @@
<tr><td>Actual deployment env</td><td><code>infra/.env</code> owner-secret fill-in</td><td>Remaining external step: deployment target must provide real <code>APP_DB_PASSWORD</code>, OAuth client id/secret, <code>OPENAI_API_KEY</code>, <code>SESSION_SECRET</code>, and production-safe engine/voice flags. Current local stray values such as <code>ENGINE_MODE=claude_cli</code> and prod sample TTS must not be copied.</td></tr>
<tr><td>Focused E2E</td><td><code>admin + db-persistence + voice-success</code></td><td>Latest admin spec: 8 passed on chromium desktop, including all-workspace admin navigation, health dashboard, real server-known users, operation tickets, mobile controls, and tablet form containment.</td></tr>
<tr><td>Dev dashboard redteam</td><td><code>scripts/test_dev_dashboard_ssot.py</code> / <code>e2e/dev-dashboard.spec.ts</code></td><td>Unit: SSOT checker 4 tests OK + current dashboard check PASS. E2E: local file, <code>127.0.0.1</code>, <code>localhost</code> 외부형 origin, 필터 오순서/빠른 재시도/키보드, details/tabs 접기, hash anchor, 링크·이미지 무결성, desktop/mobile overflow를 검증해 chromium desktop/mobile 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>Admin manage-users</td><td><code>admin.spec.ts --grep manage real server-known users</code></td><td>route-mock desktop/mobile 2 passed에 더해 2026-08-27 실제 DB/browser 1 passed. pending 계정의 `/personas` 403·`/learn→/pending`, 승인 뒤 동일 세션 200, AI 피드백 OFF PATCH·GET 영속 재조회, 계정 비활성화·active session 0과 격리 포트 종료를 확인했다.</td></tr>
<tr><td>Full E2E baseline</td><td><code>PLAYWRIGHT_PORT=5174 npm run e2e</code></td><td>2026-06-27/28 기준선 113 passed</td></tr>
<tr><td>Public auth discovery</td><td><code>E2E_PUBLIC_AUTH=1 npx playwright test --list --project=chromium-public-auth</code></td><td>2 tests listed</td></tr>
<tr><td>Public pre-auth readiness</td><td><code>public-auth-turn.spec.ts --grep production-safe</code></td><td>production-safe smoke 1 passed. 인증된 public <code>/turn</code> proof는 아직 미완료이며 owner storageState가 필요하다.</td></tr>
<tr><td>Public OAuth start</td><td><code>/auth/config</code> + <code>/auth/login?provider=google</code></td><td>2026-06-30 복구 후 auth config 200, Google configured true, redirect URI <code>https://api-vignette.chanpaca.net/auth/callback</code>, dev-login disabled. Google login start는 GET 302와 <code>__Host-vignette_oauth_state</code> HttpOnly/Secure cookie를 반환한다. Authenticated public <code>/turn</code> proof는 owner storageState가 필요하다.</td></tr>
<tr><td>Persona auth boundary</td><td><code>GET /personas</code></td><td>2026-06-30 복구 후 public unauth <code>/personas</code>는 401 <code>not authenticated</code>를 반환한다.</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 · source pin</td><td><code>start/watch/boot-public-runtime*.ps1</code> · <code>install-public-runtime-task.ps1</code> · <code>register-boot-task.ps1</code></td><td>detached-clean <code>a73bcd24…</code>·tree <code>b02b3a1b…</code>와 exact script SHA에 로그온·watchdog 두 task를 재등록했다. 두 task를 명시 실행해 <code>LastTaskResult=0</code>·Ready를 확인했고 working directory도 같은 stable root다. 실제 Windows 재부팅 후 자동복구 smoke만 별도 운영 gate로 남는다.</td></tr>
<tr><td>Public API health</td><td><code>http://127.0.0.1:8001/health</code> / <code>https://api-vignette.chanpaca.net/health</code></td><td>local/public 모두 <code>status=ok</code>, <code>db=true</code>, <code>engine=true</code>다. 공개 OpenAPI 126과 <code>/admin/voice-runtime</code>, local Whisper/MeloTTS exact provider/model, queue 4를 확인했다. DB container는 recovered named volume과 <code>unless-stopped</code> 유지한다.</td></tr>
<tr><td>Public runtime current snapshot</td><td><code>health + OpenAPI + voice + provenance + Scheduled Tasks</code></td><td>clean source <code>a73bcd24…</code>·tree <code>b02b3a1b…</code>, receipt <code>47388d58…9b08</code> passed. API/cloudflared는 같은 release root에서 실행되고 두 task result 0이다. 무마이크 rehearsal은 voice/runtime/topology를 모두 통과했으며 public health는 실행 뒤에도 ok·db/engine true다.</td></tr>
<tr><td>Public runtime scripts · source pin</td><td><code>start/watch/boot-public-runtime*.ps1</code> · <code>install-public-runtime-task.ps1</code> · <code>register-boot-task.ps1</code></td><td>2026-08-27 detached-clean <code>bf5f7352…0092</code>·tree <code>c39f917b…c88</code>와 exact boot <code>4f368b2d…5197</code>·watchdog <code>6cd3c869…8242</code>·start <code>c8c837fd…6cf1</code> SHA에 로그온·watchdog 두 task를 재등록했다. action working directory/root/commit/tree/SHA pin은 PASS, watchdog은 <code>LastTaskResult=0</code>·Ready·failcount 0이다. 부트 task는 현재 healthy surface를 불필요하게 건드리지 않도록 RunNow하지 않았고, 실제 Windows 재부팅 후 자동복구 smoke만 별도 운영 gate로 남는다.</td></tr>
<tr><td>Public API health</td><td><code>http://127.0.0.1:8001/health</code> / <code>https://api-vignette.chanpaca.net/health</code></td><td>2026-08-27 복구 후 local은 <code>status=ok·db=true·engine=true</code>, public은 같은 응답을 연속 3/3 반환했다. 실제 공개 로그인 DOM·스크린샷도 정상이며 console error/warning은 0이다. DB container는 기존 recovered named volume을 그대로 유지한다.</td></tr>
<tr><td>Public runtime current snapshot</td><td><code>health + provenance + Scheduled Tasks</code></td><td>clean source <code>bf5f7352…0092</code>·tree <code>c39f917b…c88</code>에서 API-only 복구했고 web·engine·cloudflared는 살아 있던 프로세스를 유지했다. 기존 G7 fresh receipt와 무마이크 rehearsal은 역사 증거로 보존하며, 이번 routine 복구를 새 배포·G7 승격으로 오표기하지 않는다.</td></tr>
<tr><td>Public/local/Tailnet login recovery</td><td><code>https://vignette.chanpaca.net/login</code> / <code>https://api-vignette.chanpaca.net/health</code> / <code>https://alpaca-home.taile93291.ts.net/login</code></td><td>2026-06-30 public web login HEAD 200, public API health 200, auth config 200, Google login start 302. Earlier Tailnet checks remain recorded separately; vnet DNS A records are still 0.</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>

View file

@ -82,7 +82,9 @@ vignette/
`lifespan`(startup/shutdown)에서:
1. `init_pool()` → DB 풀 생성, `ensure_runtime_tables()` / `ensure_review_tables()` 보장.
1. `init_pool()` → DB 풀 생성, 런타임 계약 readiness 확인. 개선관리 계약은
`infra/db/init/17_improvement_workbook_contracts.sql`을 owner가 선적용해야 하며,
`protocol_registry.ensure_protocol_tables()``SELECT`로 준비 상태만 확인하고 애플리케이션 역할로 DDL을 실행하지 않는다.
2. `settings.auto_seed_personas``materialize_seed_personas()`로 시스템 페르소나 P1~P3과 저장소 `data/personas/P4~P7.json``app.persona_card`에 누락분만 물리화한다. 기존 DB 저작본/보관본은 덮어쓰거나 되살리지 않는다. 운영자는 `scripts/materialize-persona-seeds.py` dry-run으로 같은 seed/version manifest를 확인하고, 명시적 `--apply`에서만 DB pool을 초기화해 이 materializer를 호출할 수 있다.
3. `engine_client.startup()` / `voice_service.startup()`로 httpx 클라이언트 준비.
4. **DB 초기화 실패 시** `environment == "dev"`이면 예외를 삼키고 경고만 남긴 채 degraded 기동한다
@ -90,8 +92,9 @@ vignette/
등록 라우터(`app/routes/`): `auth, admin, personas, sessions, teacher, users, eval, voice, kb`.
`GET /health`는 liveness + DB readiness(`db.healthcheck()`) + 엔진 게이트웨이 readiness
(`engine_client.health_detail()`)를 합쳐 `{"status": "ok|degraded", db, engine, engine_mode, ...}`를 반환한다.
`GET /health`는 liveness + DB readiness(`db.healthcheck()`) + 기본 엔진 조합과 전용 live-client 공급자의
readiness(`engine_client.health_detail()`)를 합쳐 `{"status": "ok|degraded", db, engine, engine_mode, ...}`를 반환한다.
기본 evaluator 엔진이 준비됐어도 live-client 공급자가 준비되지 않으면 `status=degraded`, `engine=false`다.
DB readiness는 auth/admin 테이블뿐 아니라 세션 read-model 핵심 테이블·컬럼
(`app.sessions`, `app.turns` 음성 메타 컬럼, `app.session_review_status` worksheet 컬럼)을 함께 확인한다.
@ -200,6 +203,9 @@ LLM 아님. 순수함수 + 작은 dataclass `SessionState`.
1. **입력 PII 마스킹** `mask_pii(text) -> MaskResult`: Presidio가 설치돼 있으면 우선 사용,
미설치면 정규식 폴백(`_PII_PATTERNS`: 주민번호/휴대폰/전화/이메일/장문 숫자열). 마스킹본만
저장·외부 LLM 전송에 쓴다(하드 게이트, F-03). Presidio는 지연 로드 캐시(`_try_load_presidio`).
`mask_role_identities()`는 현재 회기의 상담자/학습자 이름을 `[COUNSELOR]`, 페르소나/내담자 이름을
`[CLIENT]`, 그 밖의 이름을 `[NAME]`으로 정규화한다. evaluator·live-coach 입력 전과 구조화 결과 반환 전
모두 적용하며, 선택적 identity 필드는 `getattr(..., None)`으로 안전하게 처리한다.
2. **위기 분류** `classify_crisis(text, speaker_is_persona_context=True) -> CrisisResult`:
가상내담자의 자살사고 *연기*는 시뮬레이션 정상(`PERSONA_PLAY`, escalate=False). 그러나
1인칭 실제 단서(`_FIRST_PERSON_NOW`)가 강하면 **수련생 본인의 실제 위기**(`LEARNER_REAL`,
@ -373,7 +379,10 @@ session lifecycle을 유지하며, future Node read API는 이 read-model contra
- `POST /sessions``get_catalog_persona(code)`로 승인 카드 조회 → `case_profile` upsert →
case digest/직전 summary/pinned fact seed recall → `state_machine.init_state(...)`
`session_persistence.create_session(...)`. 프론트 세션 시작 전 화면은 `persona.theory_target`
`session_persistence.create_session(...)`. 생성 시 승인 페르소나의 `persona_id``persona_version`
세션에 고정하고 시작/상세 응답에도 두 값을 반환한다. 이후 더 높은 버전이 승인돼도 기존 회기는 고정된
역사 버전을 해석한다. DB 영속 생성 실패는 안전하지 않은 성공으로 흡수하지 않고 안정적인
`503 session_persistence_unavailable`로 반환한다. 프론트 세션 시작 전 화면은 `persona.theory_target`
기본값을 쓰되 학습자가 `humanistic`/`cbt`/`integrative` 중 하나를 명시 선택해 기존
`theory_mode` 계약으로 보낸다. DB가 없으면
`runtime_fallback_allowed()` 확인 후 `store.create(...)`로 in-proc 생성.
@ -390,7 +399,9 @@ session lifecycle을 유지하며, future Node read API는 이 read-model contra
순차 append → 상태 갱신. `EngineError`는 503으로 변환.
- `POST /sessions/{id}/stream` — SSE 경로(`run_turn_stream`). token/done/ping/error를 흘리고,
done 시점에 학습자 발화 + 누적 내담자 응답 + 상태를 먼저 영속화하고 즉시 완료한다. fast-loop 평가는
응답 경로 밖에서 같은 learner turn에 사후 저장한다. `sse_heartbeat_seconds`마다 ping(Cloudflare 타임아웃 회피).
응답 경로 밖에서 같은 learner turn에 사후 저장한다. provider 연결이 깨끗한 EOF로 닫혀도 canonical
gateway `done`이 없으면 `client_stream_incomplete` 오류로 끝내며 가짜 성공 턴을 저장하지 않는다.
`sse_heartbeat_seconds`마다 ping(Cloudflare 타임아웃 회피).
- `POST /sessions/{id}/live-coach` — 방금 완료된 상담자 발화를 워크북 요약/RAG/evaluator fast-loop 신호와 대조해
라이브 코칭 카드 1개를 반환하고 `app.live_coach_events``use/-1`로 저장한다. 저장 payload는 마스킹
excerpt와 코칭 구조화 JSON이며, 잔여 기회가 없으면 409로 차단한다.
@ -401,7 +412,9 @@ session lifecycle을 유지하며, future Node read API는 이 read-model contra
`kb.document/kb.chunk`로 색인한다. active hash가 같으면 skip하고, 다르면 최신 document version+1로 색인한다.
임베딩 모델이 없으면 BM25-only degraded 색인으로 진행한다.
- `POST /sessions/{id}/end``memory.make_carry_over(...)` → 세션 종료 + carry 준비 →
`_schedule_session_evaluation(sess)`로 deep-loop 평가를 비동기 태스크로 발사.
`_schedule_session_evaluation(sess)`로 deep-loop 평가를 비동기 태스크로 발사. 세션 종료 영속화가 평가보다
먼저 확정되므로 기존 회기의 deep 평가가 실패해 `error`로 남아도 그 리뷰는 degraded 상태로 읽히고,
같은 학습자는 새 회기를 생성해 턴을 이어갈 수 있다.
- 진행도 파생(P2, 2026-07-14): `session_read_model.build_session_progress(state, prev_rapport_credit,
goal_stages)`가 상태머신 수치를 학습자-안전 %로 파생한다 — 단계별 누적 게이지(현재 단계는
`rapport_credit / STAGE_ADVANCE_RAPPORT` 기준, 지나온 단계 100, 전이 대기 99 캡), 라포 누적
@ -413,7 +426,9 @@ session lifecycle을 유지하며, future Node read API는 이 read-model contra
- `GET /sessions/dashboard` — 학습자 본인 세션만 `include_turn_evaluation=true`로 집계해
`overview/growth/persona_progress/achievements/recent_feedback`를 반환한다. `overview.archived_sessions`
학습자별 보관 상태(`app.session_archive_state`) 기준 카운트이며, 보관된 종료 회기는 리뷰 대기 행동 큐에서
제외한다. 성취는 공식 등급/수료가 아니라 실제 연습 milestone만 표시한다.
제외한다. `growth.training_exposure`는 종료 회기만 집계하고 4회 미만이면 `insufficient`, 4회 이상에서
최다 페르소나 비중이 0.75 이상이면 `훈련 집중 주의`, 그 밖에는 `balanced`로 표시한다. 투명한 노출 비중이지
공정성·임상 진단이 아니다. 성취는 공식 등급/수료가 아니라 실제 연습 milestone만 표시한다.
- `GET /sessions/{id}/review` — 저장된 축어록 + 평가 AI 산출물을
`session_read_model.build_session_review(...)`가 학습자-안전 리뷰로 구성한다. 리뷰 조회 시 발화별 fast-loop 평가는
`app.feedback_scores`/라벨 조인 테이블에서 `TurnRecord.evaluation` 형태로 hydrate한다.
@ -423,10 +438,24 @@ session lifecycle을 유지하며, future Node read API는 이 read-model contra
`ReviewNote.body`는 제한 markdown(`code`, `strong`, blockquote, list)으로 렌더링하고,
`effective_openness` 같은 내부 수치는 `effective openness(유효 개방도)`로 학술 용어화한다.
상담자 질문/발화 근거는 `ReviewNote.quote`로 분리해 리뷰 카드 안에서 인용 블록으로 표시한다.
첫 회기에는 `first-session-rapport-open-question.v1` 체크리스트가 라포 반영과 한 초점 열린 질문을
결정론적으로 관찰하고 근거 turn을 연결한다. 관찰되지 않으면 `not_observed`, 2회기 이후면
`not_applicable`이며 임상 점수가 아닌 교육 체크리스트다.
학습자의 AI 피드백 노출은 현재 계정 설정과 세션 시작 시점 snapshot이 모두 켜진 경우에만 허용한다.
여러 회기를 합치는 학습자 집계도 같은 AND를 각 원천에 적용한다. `calibration/learners/me`는 과거 OFF
원천 또는 실행 회기가 하나라도 섞이면 learner-input-only로 fail-closed해 AI·교수자 파생값을 제거하고,
`practice/learners/me`는 처방·에피소드·최신 역량 그래프의 원천 회기 중 하나라도 OFF이면 403을 반환한다.
일반 practice attempt 제출도 현재 계정과 처방 원천 회기 snapshot이 모두 ON이어야 한다. 그 밖에 OFF인
순수 AI 파생 엔드포인트는 403이고 혼합 응답은 사용자가 입력한 calibration/alliance/multimodal 필드만
보존한 채 AI 결과·근거·측정을 제거한다. 축어록, 저장된 학습자 워크시트 원문, alliance 자기보고,
outcome 관찰 입력, calibration 예측·수정·잠금·실행 입력, multimodal 동의·철회·삭제·raw audio·privacy
ledger, privacy export/delete는 유지한다. teacher/admin 감독 뷰에는 이 learner gate를 적용하지 않는다.
- `POST /sessions/{id}/share` — 학습자가 종료된 본인 회기 리뷰를 URL로 공유하기 위해 공개 토큰을 생성한다.
서버는 `session_read_model.session_share_payload(...)`로 preview를 정규화한 뒤 `app.session_share_link`
토큰 해시와 sanitized preview payload만 저장하고, 원문 축어록·학습자
식별자는 payload에 넣지 않는다. 토큰은 생성 응답에서만 반환되며 새 생성은 기존 토큰을 교체한다.
학습자 AI 피드백이 현재 계정 또는 세션 snapshot에서 OFF이면 새 공유를 거부하고, ON일 때 이미 만든
토큰도 public load 시 같은 두 정책을 다시 확인해 404로 닫는다.
- `DELETE /sessions/{id}/share` — 해당 회기의 공개 공유 토큰을 폐기한다.
- `POST /sessions/{id}/archive` / `POST /sessions/{id}/restore` — 학습자 본인의 종료 회기를 보관/복원한다.
진행 중 회기는 409로 거부한다. 보관은 `app.session_archive_state`만 upsert/delete하는 보기 상태이며,
@ -513,7 +542,8 @@ GET {ENGINE_URL}/ready|/health — readiness/liveness
`scripts/check-engine-gateway-contract.mjs`는 같은 artifact를 Node.js에서 Python import 없이 검증하므로
미래 Node.js gateway의 최소 conformance gate로 쓴다.
`/v1/stream` 종료는 provider pass-through `data: [DONE]`가 아니라 `event: done` + JSON telemetry
payload로 변환해야 한다.
payload로 변환해야 한다. provider의 `[DONE]`은 호환 입력일 뿐 canonical 완료가 아니며, 구조화 `done`
없이 EOF가 오면 앱은 `client_stream_incomplete`로 실패 처리한다.
브라우저로 재방출되는 `/sessions/{id}/stream` SSE는 별도 앱 계약이며, engine gateway SSE의
JSON token payload와 섞지 않는다.
@ -549,6 +579,8 @@ HTTP error mapping을 유지한다.
- `get_approved_persona(code)` / `list_approved_personas()` — 승인된 최신 버전 조회(AI 컨텍스트).
- `get_catalog_persona(code)` — 승인 카드 조회, 실패 시 `settings.allow_seed_persona_fallback`이면
`seed_fallback_persona`(degraded=True)로 폴백.
- `persona_read_model._presenting_summary()` — 카드 JSON의 키 순서와 무관하게 `complaint`/`주호소` 계열을
우선해 제시문제를 구성한다. `surface` 같은 표면 상태를 주호소로 오인하지 않는다.
- 페르소나 워크스페이스: `/teach/personas`는 teacher/admin 전용 운영·저작 작업면이다. 상단 horizontal tab이
`대시보드`(공개/활성 페르소나·학습 인원·세션·평가/라포·검수 요약), `카탈로그`(공개 규칙·구성·버전),
`페르소나`(검색/필터 가능한 전체 table + 내부 `검수 현황` 탭)를 나눈다. 행을 누르면 소개·학습 현황·설정
@ -574,6 +606,21 @@ HTTP error mapping을 유지한다.
- 교수 검수: `list_persona_review_queue(role)` / `update_persona_review_status(action=approve|reject)`
— 승인/반려 시 `audit.audit_log`에 감사 기록.
### 2.13 개선관리 프로토콜 레지스트리 — `app/services/protocol_registry.py`
관리자 전용 `/admin/protocols`가 프로토콜 목록, draft 생성, 활성화, 폐기를 제공한다. 상태는
`draft → active → retired` 단방향이며 retired 항목은 다시 활성화하지 않는다.
- 생성 내용은 서버에서 정규화하고 SHA-256을 계산한다. 라이선스 분류 A/B/C/D와
`external_llm_ok`를 함께 저장하며 C/D는 요청·서비스·DB 제약 모두에서 외부 LLM 사용을 거부한다.
- 활성화는 한 트랜잭션에서 `kb.source`를 등록하고 RAG `kb.document/kb.chunk` 색인을 완료한 뒤에만
`active`로 바꾼다. 색인 실패는 전체 rollback하며 동시 활성화는 직렬화돼 하나만 성공한다.
- chunk는 `visible_to=['evaluator']`, `sensitivity=2`이고 라이선스·외부 LLM 허용 메타데이터를 보존한다.
외부 LLM에 보낼 수 없는 근거는 retrieval 후에도 prompt assembly에서 제외한다.
- 폐기는 연결 문서를 비활성화한 뒤 terminal `retired`로 전이하고, 중간 실패는 rollback한다.
- `ensure_protocol_tables()`는 migration 17 계약을 조회할 뿐 DDL을 만들지 않는다. 누락 시 적용해야 할
migration 파일을 명시해 fail-closed한다.
---
## 3. 엔진 게이트웨이 — `apps/api/engine_gateway/gateway.py`
@ -713,8 +760,8 @@ React 19 + Vite. 라우팅은 `apps/web/src/App.tsx`(react-router-dom).
## 5. 데이터베이스 스키마 개요
DB는 PostgreSQL 16 + pgvector(단일 SoR). 초기화 SQL은 `infra/db/init/`순서대로 적용된다
(`01_extensions → 02_schema → 03_kb → 04_audit_eval_rls → 05_runtime_auth → 06_session_evaluation → 07_measurement_foundation`).
DB는 PostgreSQL 16 + pgvector(단일 SoR). 초기화 SQL은 `infra/db/init/`번호순으로 적용되며,
개선관리 계약까지 필요한 현행 끝점은 `17_improvement_workbook_contracts.sql`이다.
스키마는 **4분할**: `app` / `kb` / `audit` / `ds`.
### 5.1 app 스키마 (`infra/db/init/02_schema.sql`)
@ -865,6 +912,14 @@ DB는 PostgreSQL 16 + pgvector(단일 SoR). 초기화 SQL은 `infra/db/init/`에
fact만 허용하고, 삽입/값 변경 history와 명시적 상담 약속 철회 contradiction까지 남긴다.
관계·임상 fact 승격과 광범위 자동 모순 판정은 후속이다.
### 5.1.1 개선관리 migration 17
`infra/db/init/17_improvement_workbook_contracts.sql``app.app_user``app.sessions`
`learner_feedback_enabled`, 프로토콜 레지스트리와 라이선스/외부 LLM 제약, lifecycle timestamp/index,
관리자 전용 `kb.chunk` write RLS를 멱등하게 추가한다. 새 DB volume은 번호순 init으로 적용되고,
기존 DB는 owner가 단일 트랜잭션으로 실행한다. 애플리케이션 역할 startup은 readiness만 확인하며 runtime
DDL로 빠진 계약을 보충하지 않는다. 누락되면 migration 17 적용을 요구하며 fail-closed한다.
### 5.2 audit 스키마 + app 평가/종단 (`infra/db/init/04_audit_eval_rls.sql`)
- 평가: `app.feedback_scores`(발화별 점수·rationale, `visible_to='{evaluator}'`, loop fast/deep),
@ -916,7 +971,8 @@ DB 레벨 이중강제(`04_audit_eval_rls.sql` §5, `app/db.py` `acquire()`):
그래서 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`),
서버 로그에 남긴다. Uvicorn access logger에는 별도 필터를 설치해 `/auth/callback`과 후행 슬래시 변형의
query 전체를 제거하되 다른 요청의 query는 보존한다. 프론트는 `token_exchange_failed`, `invalid_state`, provider error(`access_denied`/`provider_error`),
identity claim 실패를 구분하고 실패 reason code를 화면에 함께 표시한다.
- 역할은 `AUTH_TEACHER_EMAILS`/`AUTH_ADMIN_EMAILS` email allowlist로 1차 판정한다.
실제 `admin` 역할 사용자는 관리자 콘솔, 교수자 공간, 학습자 공간에 모두 접근할 수 있다.
@ -927,6 +983,10 @@ DB 레벨 이중강제(`04_audit_eval_rls.sql` §5, `app/db.py` `acquire()`):
`AUTH_EMAIL_COHORT_MAP``AUTH_DOMAIN_COHORT_MAP` 설정, SAML fixture의 `cohort` claim을 합쳐
`cohort_ids`로 세션에 저장한다. 관리 사용자 `app_user.external_id`는 provider subject 기반
(`google:`/`saml:`/`dev:`)으로 저장해 email 변경 리스크를 줄인다.
- 관리자 외부 연구참여자 사전등록 `POST /admin/users`는 요청값과 무관하게 항상
`account_status=pending`으로 정확한 이메일 계정을 만든다. 승인·정지는 별도
`PATCH /admin/users/{user_id}`에서만 수행해 create와 승인 권한 효과를 분리한다. 이는 공개 무제한 가입
엔드포인트가 아니며 관리자 콘솔의 사전등록 탭과 승인 큐가 같은 경계를 따른다.
- 프론트의 최초 진입 경로(`initialPathForUser`)는 pending이면 `/pending`, 온보딩 미완료 일반 사용자는
`/onboarding`, 관리자 콘솔 접근권이 있는 사용자는 기본 역할이 learner/teacher여도 `/admin`을 우선한다.
역할 전환용 `roleHomePath`는 그대로 역할별 홈(`/learn`, `/teach`, `/admin`)만 소유한다.
@ -969,11 +1029,13 @@ DB 레벨 이중강제(`04_audit_eval_rls.sql` §5, `app/db.py` `acquire()`):
- OAuth/SAML callback은 저장된 `next`가 일반 진입 경로(`/`, `/learn`, `/teach`, `/login`, `/onboarding`)이고
로그인 사용자가 관리자 콘솔 접근권을 가지면 `/admin`으로 정규화한다. 단, `/learn/session/...` 같은 깊은
링크는 사용자가 의도적으로 연 URL일 수 있으므로 보존한다.
- `AUTH_ALLOWED_EMAIL_DOMAINS`는 기본 도메인 게이트다. 단, 슈퍼 관리자/관리자가 `/admin/users`
미리 만든 정확한 이메일은 도메인 밖이어도 Google/SAML/dev-login의 이메일 검증을 통과한다.
이 예외는 도메인 전체를 열지 않고, provider 로그인 시 기존 `email:<주소>` 관리 row를
`google:`/`saml:`/`dev:` external_id로 이어받아 역할·코호트·승인 상태를 보존한다.
- 신규 Google/SAML 사용자는 기본적으로 `account_status=pending`으로 생성된다
- Google OIDC는 ID token의 issuer·audience·서명 검증 경로와 `email_verified`를 통과한 모든 이메일을
도메인·사전등록 없이 허용한다. 신규 사용자는 learner·`approved`로 만들고, 기존 pending row를 Google
external id로 연결할 때도 approved로 승격한다. 기존 inactive/suspended 계정은 이 경로로 우회하지 못한다.
- `AUTH_ALLOWED_EMAIL_DOMAINS`는 dev-login·SAML 조직 정책용 게이트다. 슈퍼 관리자/관리자가
`/admin/users`에 미리 만든 정확한 이메일은 이 비-Google 경계에서 도메인 밖 예외로 허용하고,
`email:<주소>` 관리 row를 provider external id로 이어받아 역할·코호트·승인 상태를 보존한다.
- SAML 등 비-Google 신규 사용자는 기본적으로 `account_status=pending`으로 생성된다
(`AUTH_NEW_USER_DEFAULT_STATUS`). `/auth/me`만 pending 상태 확인용으로 열어두고, 그 외 REST/WS 기능
경로는 `account_pending` 또는 인증 실패로 막는다. `AUTH_APPROVED_EMAILS`, 관리자 생성 사용자,
`AUTH_SUPER_ADMIN_EMAILS`, 로컬 dev-login(`external_id=dev:*`)은 자동 approved다.
@ -1086,4 +1148,9 @@ RAG 임베딩/리랭커 의존성은 기본 슬림 이미지에 넣지 않고 `I
- 평가/정답/평가 전용 데이터는 RBAC×AIView(레이어1 `visible_to`) + RLS(레이어2)로 학습자 직접 조회에서 차단된다.
단, 서버 리뷰 응답은 소유 세션 확인 후 evaluator 컨텍스트로 필요한 평가 rows만 hydrate해 학습자-안전 형태로 가공한다.
- 평가/로깅 훅 실패는 비치명적으로 흡수되어 상담 루프를 멈추지 않는다.
- 회기 종료 영속화는 deep 평가보다 먼저 확정한다. 오래된 회기의 평가 실패는 error/degraded 리뷰로 남지만
새 회기 생성과 턴 저장을 막지 않는다.
- 학습자 AI 파생 피드백은 현재 계정 설정과 세션 snapshot의 논리 AND다. 현재 OFF가 과거 ON보다 우선하고,
순수 파생 API는 403, 기존 공개 share token은 404다. 사용자 원문/입력·privacy 조작은 보존하며
teacher/admin 감독 뷰는 유지한다.
```

View file

@ -177,10 +177,10 @@ npm install
| `ENGINE_GATEWAY_SHARED_SECRET` | 빈 값 | 선택 인증. NAS/원격 preview에서는 API와 gateway에 동일한 32자 이상 비-placeholder 값을 설정. 빈 값은 기존 로컬 9099 호환 |
| `VIGNETTE_LIVE_CLIENT_PROVIDER` | `claude_cli` | 실시간 내담자 AI 전용 lane. 관리자에서 선택한 evaluator/review 공급자와 분리해 회기별 Claude 상주 세션을 재사용 |
| `AUTH_DEV_LOGIN_ENABLED` | `true` | dev-login 엔드포인트 활성화 |
| `AUTH_ALLOWED_EMAIL_DOMAINS` | `["hs.ac.kr","twentyoz.kr"]` | 기본 로그인 허용 이메일 도메인(dev-login 포함 검증). `/admin/users`에 미리 등록된 정확한 이메일은 도메인 밖이어도 예외로 로그인 가능 |
| `AUTH_ALLOWED_EMAIL_DOMAINS` | `["hs.ac.kr","twentyoz.kr"]` | dev-login·SAML 조직 정책용 도메인 목록. Google OIDC는 이 목록을 적용하지 않고 provider-verified 이메일을 모두 허용 |
| `AUTH_SUPER_ADMIN_EMAILS` | `["yunchan@twentyoz.kr","hoonjungkoo@hs.ac.kr"]` | 학습자·교수자·관리자 공간 접근과 승인 상태를 부여할 슈퍼 관리자 이메일 |
| `AUTH_APPROVED_EMAILS` | `[]` | 신규 외부 로그인 시 pending 없이 바로 승인할 이메일 allowlist |
| `AUTH_NEW_USER_DEFAULT_STATUS` | `pending` | Google/SAML 신규 사용자의 기본 승인 상태. `dev:` 로그인은 로컬/E2E 편의를 위해 자동 승인 |
| `AUTH_APPROVED_EMAILS` | `[]` | SAML/dev 등 비-Google 신규 로그인에서 pending 없이 바로 승인할 이메일 allowlist |
| `AUTH_NEW_USER_DEFAULT_STATUS` | `pending` | SAML 등 비-Google 신규 사용자의 기본 승인 상태. Google은 항상 approved, `dev:` 로그인은 로컬/E2E 편의를 위해 자동 승인 |
| `AUTH_EMAIL_COHORT_MAP` | `{}` | 특정 이메일을 cohort id로 매핑한다. 값은 comma-separated 문자열도 허용 |
| `AUTH_DOMAIN_COHORT_MAP` | `{}` | 이메일/Google hosted domain을 cohort id로 매핑한다. Google/SAML/dev-login 세션 `cohort_ids`에 반영 |
| `VIGNETTE_MELOTTS_TTS_URL` | `http://127.0.0.1:9883` | 로컬 MeloTTS 한국어 사이드카. `scripts/start-melotts.ps1`로 띄운다 |
@ -356,20 +356,25 @@ C:\Users\encep\AppData\Local\Programs\Python\Python311\python.exe scripts\mainta
- `role` 기본값은 `learner`.
- 성공 시 `__Host-vignette_sid` 쿠키(및 dev 전용 `vignette_sid` 쿠키)를 세팅한다.
- dev-login 사용자는 로컬/E2E 흐름 유지를 위해 `account_status=approved`로 생성된다.
- Google/SAML 신규 사용자는 기본적으로 `account_status=pending`이며, 승인 전에는 `/pending` 화면만 볼 수 있다.
- Google OIDC 신규 사용자는 provider가 이메일을 검증하면 도메인·사전등록 없이 learner·`approved`로 생성된다.
기존 pending Google 계정도 로그인 시 approved로 승격하지만 suspended 계정은 그대로 차단한다.
- SAML 등 비-Google 신규 사용자는 기본적으로 `account_status=pending`이며, 승인 전에는 `/pending` 화면만 볼 수 있다.
관리자 콘솔 `/admin/users`의 가입 승인 탭에서 `approved`로 바꾸면 역할 홈에 접근한다.
DB가 연결되어 있으면 pending 생성 시 `app.notification_event`/`app.notification_delivery`에 가입 승인
메일 큐가 생긴다. `NOTIFICATION_EMAIL_PROVIDER=disabled`인 로컬 기본값에서는 실제 메일은 발송하지 않는다.
- 관리자 콘솔의 외부 연구참여자 사전등록은 `POST /admin/users`에서 항상 `pending`으로만 생성한다.
생성 요청으로 즉시 승인할 수 없고, 관리자가 승인 큐에서 별도 `PATCH /admin/users/{user_id}`를 보내야
`approved`가 된다. exact-email 사전등록이지 공개 무제한 회원가입이 아니다.
- 신규 사용자 또는 온보딩 미완료 사용자는 로그인 직후 `/onboarding`에서 닉네임, 자기소개,
선택 아바타 이미지, 이름, 소속, 학과, 학년/직위, 전화번호, 주소/수령지와 약관·개인정보
동의를 저장해야 역할 홈으로 이동한다. 학습자 회기 시작은 온보딩 완료와 동의가 모두 있어야 한다.
- 프로필 아바타 업로드는 API 작업 디렉터리 기준 `USER_UPLOAD_DIR`(기본 `uploads`) 아래
`profile-avatars/`에 저장되고, `/uploads/profile-avatars/...` URL로 서빙된다.
> 주의(이메일 도메인): dev-login도 기본적으로 `validate_google_identity_domain`을 거치므로
> 주의(이메일 도메인): 이 제한은 dev-login·SAML 조직 정책에만 적용된다. dev-login은
> 이메일 도메인이 `AUTH_ALLOWED_EMAIL_DOMAINS`에 있어야 한다. 예: `learner@hs.ac.kr`.
> 단, 관리자가 `/admin/users`에 미리 등록한 정확한 이메일은 도메인 밖이어도 로그인할 수 있다.
> 미등록 외부 도메인은 계속 `403 email domain is not allowed`.
> Google OIDC는 provider-verified 이메일이면 도메인과 사전등록 여부를 묻지 않는다.
### 3.1 메일 알림 큐 확인/처리
@ -470,6 +475,24 @@ DB 없이 CLI shape만 확인하려면:
py -3.11 scripts\sync-persona-sources.py --help
```
### 3.6 개선관리 migration 17과 프로토콜 레지스트리
새 Postgres volume은 `infra/db/init/`의 번호순 init으로 migration 17까지 적용한다. 이미 존재하는 DB는
애플리케이션 역할이 startup에서 테이블을 만들지 않으므로 owner DSN으로 한 번 적용해야 한다.
```powershell
cd D:\workspace\vignette
psql.exe "$env:VIGNETTE_OWNER_DATABASE_URL" -v ON_ERROR_STOP=1 --single-transaction `
-f infra\db\init\17_improvement_workbook_contracts.sql
```
`protocol_registry.ensure_protocol_tables()`는 readiness `SELECT`만 실행한다. 계약이 없으면 migration 17을
적용하라는 오류로 fail-closed하며 app-role DDL fallback은 없다. 적용 후 admin dev-login으로
`GET /admin/protocols`, `POST /admin/protocols`, `POST /admin/protocols/{id}/activate`,
`POST /admin/protocols/{id}/retire`를 사용할 수 있다. lifecycle은 `draft → active → retired`이고,
활성화는 라이선스·`external_llm_ok` 검증과 evaluator-only RAG 색인이 한 트랜잭션에서 성공해야 끝난다.
라이선스 C/D는 외부 LLM 사용을 허용할 수 없다.
---
## 4. 웹(프런트엔드) 실행
@ -540,9 +563,11 @@ Invoke-RestMethod 'http://127.0.0.1:9099/ready?provider=codex_cli&model=gpt-5.6-
Invoke-RestMethod 'http://127.0.0.1:9099/v1/capabilities?provider=codex_cli&force=true' -Headers $headers
```
API의 `/health`는 현재 DB 설정의 provider/model/reasoning_effort를 게이트웨이 `/ready`에 전달하고
(구형 게이트웨이는 `/health`로 폴백) `engine` 필드를 채운다. 따라서 프로세스만 떠 있고 선택한 CLI/API가
미인증이거나 모델 조합을 실행할 수 없으면 API `/health``engine: false`가 된다.
API의 `/health`는 현재 DB 설정의 provider/model/reasoning_effort와
`VIGNETTE_LIVE_CLIENT_PROVIDER` 전용 내담자 lane을 각각 게이트웨이 `/ready`로 확인한다
(구형 게이트웨이는 `/health`로 폴백). 기본 evaluator 조합이 준비됐더라도 live-client 공급자가 준비되지
않으면 `status=degraded`, `engine=false`다. 따라서 프로세스만 떠 있고 선택한 CLI/API가 미인증이거나
어느 필수 모델 조합도 실행할 수 없으면 준비 완료로 보지 않는다.
### 5.3 프로빙 스크립트 (선택)
@ -610,8 +635,8 @@ RAG 임베딩/리랭커 의존성은 기본 이미지에 설치하지 않는다.
```powershell
# 백엔드 (apps/api)
cd apps\api
python -m pytest app/ -q # 백엔드 기준선 432 pass
python -m pytest engine_gateway\ -q # 현재 44 pass
python -m pytest app/ -q # 2026-08-28 전체 실행 1002 passed
python -m pytest engine_gateway\ -q # 2026-08-28 전체 실행 68 passed
# 웹 (apps/web)
cd apps\web
@ -636,7 +661,15 @@ npm run e2e # Playwright — web+api+DB 스택 필요
아니면 폴백하지 않고 기동이 실패한다.
- **`/health``engine: false` / 턴 생성 실패**: 게이트웨이(9099)가 안 떠 있거나 `claude` CLI가
인증 안 됨. 게이트웨이 `GET /ready``detail`을 보고 원인을 확인한다. 게이트웨이를 먼저 실행하라.
인증 안 됐거나 기본 evaluator/live-client 중 하나가 준비되지 않음. 게이트웨이 `GET /ready``detail`
`VIGNETTE_LIVE_CLIENT_PROVIDER`를 함께 확인한다. 게이트웨이를 먼저 실행하라.
- **SSE가 token 뒤 조용히 끝나고 턴이 저장되지 않음**: gateway의 구조화 `done` 없이 EOF가 온 경우다.
서버는 `client_stream_incomplete`로 fail-closed하며 provider `[DONE]`만으로 성공 처리하지 않는다.
게이트웨이 로그와 `/ready`를 확인하고 재시도하라.
- **프로토콜 레지스트리 readiness 실패 / migration 17 요구**: app-role로 DDL을 시도하지 않는다.
기존 DB에 owner DSN으로 `17_improvement_workbook_contracts.sql`을 적용한 뒤 API를 다시 기동한다.
- **dev-login이 404 (`dev login is disabled`)**: `ENVIRONMENT=dev` + `AUTH_DEV_LOGIN_ENABLED=true`인지,
요청이 로컬 Origin/Host인지 확인. `apps/api`에서 uvicorn을 실행해 `.env`가 로드됐는지도 확인.

View file

@ -15,12 +15,12 @@ Vignette 저장소의 모든 검증 수단(백엔드 단위 테스트, 웹 타
| 검증 | 작업 디렉터리 | 명령 | DB | API(8000) | 웹(5173) | 엔진GW(9099) | 브라우저 | 현재 통과 |
|---|---|---|---|---|---|---|---|---|
| 백엔드 단위 테스트 | `apps/api` | `python -m pytest app/ -q` | 불필요 | 불필요 | 불필요 | 불필요 | 불필요 | 2026-07-31 전체 실행 439 passed |
| 엔진 게이트웨이 테스트 | `apps/api` | `python -m pytest engine_gateway/ -q` | 불필요 | 불필요 | 불필요 | 불필요 | 불필요 | 2026-07-31 전체 실행 45 passed |
| 백엔드 단위 테스트 | `apps/api` | `python -m pytest app/ -q` | 불필요 | 불필요 | 불필요 | 불필요 | 불필요 | 2026-08-28 전체 실행 1002 passed |
| 엔진 게이트웨이 테스트 | `apps/api` | `python -m pytest engine_gateway/ -q` | 불필요 | 불필요 | 불필요 | 불필요 | 불필요 | 2026-08-28 전체 실행 68 passed |
| API 타입 생성 체크 | `apps/web` | `npm run check:api-types` | 불필요 | 불필요 | 불필요 | 불필요 | 불필요 | pass |
| 웹 타입체크 | `apps/web` | `npm run typecheck` | 불필요 | 불필요 | 불필요 | 불필요 | 불필요 | pass |
| 웹 빌드 | `apps/web` | `npm run build` | 불필요 | 불필요 | 불필요 | 불필요 | 불필요 | pass |
| Playwright E2E(전체) | `apps/web` | `npm run e2e` | **필요(+시드)** | **필요** | 자동기동 | 일부만 | **필요** | 현재 수집 1111 tests / 61 files · 현 작업트리 전체 GREEN 미검증 |
| Playwright E2E(전체) | `apps/web` | `npm run e2e` | **필요(+시드)** | **필요** | 자동기동 | 일부만 | **필요** | 현재 수집 1136 tests / 62 files · 현 작업트리 전체 GREEN 미검증 |
핵심 원칙: **단위 테스트(pytest)와 타입체크/빌드는 외부 서비스 없이 단독 실행된다.**
**E2E만 풀스택(DB+API+웹+브라우저)을 요구한다.** 아래 각 절에서 근거와 절차를 설명한다.
@ -42,17 +42,20 @@ Vignette 저장소의 모든 검증 수단(백엔드 단위 테스트, 웹 타
```sh
# apps/api
python -m pytest app/ -q # 앱 단위 테스트: 2026-07-31 전체 실행 439 passed
python -m pytest engine_gateway/ -q # 게이트웨이 단위 테스트: 2026-07-31 전체 실행 45 passed
python -m pytest app/ -q # 앱 단위 테스트: 2026-08-28 전체 실행 1002 passed
python -m pytest engine_gateway/ -q # 게이트웨이 단위 테스트: 2026-08-28 전체 실행 68 passed
```
수집만 빠르게 확인하려면:
```sh
python -m pytest app/ --collect-only -q # 현재: "436 tests collected" (2026-07-31)
python -m pytest engine_gateway/ --collect-only -q # 현재: "45 tests collected" (2026-07-31)
python -m pytest app/ --collect-only -q
python -m pytest engine_gateway/ --collect-only -q
```
수집량은 위 명령의 현재 출력으로 확인한다. `--collect-only` 숫자는 통과 수가 아니므로 실행 결과와 섞어
기록하지 않는다.
> 참고: 실행 중 `PendingDeprecationWarning: Please use 'import python_multipart'`
> 경고가 보일 수 있으나 무해하며 통과 결과에 영향을 주지 않는다.
@ -75,6 +78,32 @@ python -m pytest engine_gateway/ --collect-only -q # 현재: "45 tests collected
| `app/test_session_digest_worker.py` | M2 session digest worker 요청 계약, accepted-only 적용, raw text 차단, 재실행 방지 |
| `scripts/check-dev-dashboard-ssot.py --json` | `docs/dev_dashboard.html` 상태 카운트·M2 검증 수치·DONE/GATE stale 문구 guard |
#### 개선관리 워크북 focused 증거 (2026-08-27~28)
아래는 전체 1002/68과 별도로 해당 계약을 좁혀 실행한 확정 증거다. 같은 묶음의 테스트가 여러 요구사항
경계를 함께 검증할 수 있으므로 숫자를 요구사항별 전체 합계로 더하지 않는다.
| 항목 | focused 명령/파일 | 확인 결과 |
|---|---|---|
| C-002 | `app/test_first_session_checklist.py` | 3 passed — 첫 회기 라포 반영·한 초점 열린 질문, evidence turn, 이후 회기 not-applicable |
| C-003 | `app/test_learner_dashboard.py` | 11 passed — 종료 회기 4건 전에는 insufficient, 이후 dominant share 0.75 경계 |
| REQ-001 | `app/test_auth_providers.py`, `app/test_access_logging.py`, OAuth UI focused E2E, `e2e/admin.spec.ts`/`e2e/uc-admin-console.spec.ts` | auth 41 + access-log redaction 6 pytest, OAuth UI desktop/mobile 4, route-mock browser 2, 실제 DB/browser 1 passed — 모든 provider-verified Google 이메일을 도메인·사전등록 없이 learner·approved로 허용하고 suspended는 보존. callback query는 Uvicorn access log에서 제거. 관리자 사전등록 create는 pending 고정이며 승인 전 `/personas` 403·`/learn→/pending`, 별도 PATCH 승인 뒤 같은 세션 `/personas` 200, `learner_feedback_enabled=false` 영속 재조회, 비활성화·활성 세션 0 |
| REQ-002·005 | `app/test_protocol_registry.py app/test_persona_session_contract.py` | 26 passed(경고 1); persona pin·ideation runtime clamp 계약 단독 4 passed(경고 1) |
| REQ-002 실제 DB | `e2e/persona-db-lifecycle.spec.ts`의 protocol lifecycle | 1 passed(10.1s) — draft 생성→RAG 활성화→폐기와 DB 정리 |
| REQ-003·005 실제 DB | `e2e/persona-db-lifecycle.spec.ts`의 persona lifecycle | 1 passed(14.4s), mock/skip 0 — 교수자 작성→검수 승인→catalog v1, complaint 우선 주호소·surface 미노출, 학습자 prestart, exact persona ID/version session pin, controlled 실제 SSE, UI exact reply, DB `2|counselor,client`; 종료 후 사용자·페르소나·세션·설정 marker 0과 엔진 설정 exact 원복 |
| REQ-003 | `app/test_persona_review.py` | 34 passed — complaint 우선 read model과 surface 오인 방지 포함 |
| REQ-004 | `app/test_feedback_policy.py` 포함 직접 정책 subset, 관련 route/read-model, `e2e/session-review.spec.ts` desktop·mobile | 85 + 163 pytest, browser 2 passed — learner OFF 직접 API 403, 과거 OFF source가 섞인 calibration 집계의 learner-input-only redaction, deliberate-practice 원천 snapshot OFF 집계·제출 403, alliance self-scores-only, 파생 endpoint별 요청 0회, 입력·privacy 보존, 삭제 조작면 44px·overflow 0 |
| REQ-006 | `app/test_pii_masking_eval.py app/test_live_coach_sources.py app/test_client_reply_quality.py` | 24 passed — raw prompt와 구조화 결과의 counselor/client role masking |
| REQ-007 | session persistence/evaluation 관련 API 9 files, live/session 묶음, 실제 DB persistence E2E | 163 + 50 pytest, DB E2E 1 passed — 실패한 옛 평가 뒤 새 회기 가능 |
| REQ-008 | `app/test_engine_health_contract.py`와 session stream 회귀 | health contract 1 passed; incomplete EOF는 live/session 50 묶음에 포함 |
로컬 Playwright는 기존 포트의 서버를 기본 재사용하지 않는다. 공개 preview나 다른 checkout의 stale bundle을
현 작업트리 증거로 오인하지 않도록 충돌 시 fail-closed하며, 동일 dev server를 의도적으로 공유할 때만
`PLAYWRIGHT_REUSE_EXISTING_SERVER=1`을 명시한다. Windows/npm 11에서는 Vite host/port를 등호형 인자로 전달한다.
REQ-005의 `persona_id/persona_version` API 계약과 영속 pin은 focused 테스트와 실제 DB 브라우저 lifecycle에서
모두 확인했다. 통합 실행은 승인 catalog의 exact ID/version, controlled 실제 SSE, UI 응답, DB 2턴과 cleanup을 함께 고정한다.
### 1.4 `engine_gateway/` 테스트
| 파일 | 검증 영역 |
@ -271,7 +300,7 @@ VITE_API_BASE=http://127.0.0.1:8000 npm run e2e # 프록시 대신 API
### 3.6 실측 테스트 개수 (현재)
2026-08-18 `npx playwright test --list` 기준 **현재 수집 1111 tests / 61 files**다
2026-08-27 `npx playwright test --list` 기준 **현재 수집 1136 tests / 62 files**다
(유스케이스 16테마 `uc-*.spec.ts` 239 시나리오 포함).
이 숫자는 수집량이지 통과량이 아니다. 현 작업트리 전체 1109개 완주는 아직 증거가 없으며,
과거 전체 GREEN 기록과 이번 focused/release gate 결과를 구분해 적는다.
@ -527,7 +556,7 @@ hourly heartbeat는 최신 GREEN이 6시간 이상 오래됐거나 material mile
| 세션 레이아웃 | `e2e/session-layout.spec.ts` | 4 테스트 × (desktop+mobile) | **8 / 8** |
| 시각 레이아웃 게이트 | `e2e/layout-visual-gate.spec.ts` | `@single-run`, 15개 화면 계약 × 7개 폭 검사 + 다크 테마 assertion + 학습 대시보드 라이트 자산 연결 | **15 / 15** |
| 인증 테마 게이트 | `e2e/auth-visual.spec.ts` | 로그인·온보딩 × light/dark × 390/1280px, 100vw/100dvh 및 overflow 검사 | **1 / 1** |
| 레이아웃 포커스(재설계 화면) | `session-layout`·`session-review`·`admin`·`learner`·`settings`·`teacher`, `@single-run` 제외 | desktop+mobile 병렬 | **54** |
| 레이아웃 포커스(재설계 화면) | `session-layout`·`session-review`·`admin`·`learner`·`settings`·`teacher`, `@single-run` 제외 | desktop+mobile, 격리 API/controlled gateway | **106 / 106** |
> `layout-visual-gate`는 7개 폭(390/720/861/900/1024/1280/1440)에서 15개 핵심 화면 계약(페르소나 운영·작성 단계 포함)의 가로
> 오버플로·잘린 컨트롤·다크 테마 적용을 검사하고 전체 페이지 스크린샷을
@ -536,6 +565,9 @@ hourly heartbeat는 최신 GREEN이 6시간 이상 오래됐거나 material mile
> `session-layout`은 회기 전/활성 화면이 뷰포트를 벗어나지 않는지, 우측 패널이 코어 영역을
> 침범하지 않는지, 시작 후 실제 `session_id` URL에서 새로고침해도 활성 회기 상세가 유지되는지,
> 스트림 실패 시 미저장 전사가 남지 않는지를 검증한다.
> 2026-08-28 최종 재실행은 `layout-visual-gate` 15/15, 위 레이아웃 포커스 106/106,
> `session-layout` 8/8을 실패·skip 0으로 통과했다. 모바일 관리자 topbar와 세션 입력/44px 제어,
> P12 ideation DB 범위 clamp를 실제 회귀로 고정했으며, 격리 포트와 프로세스는 모두 종료했다.
### 3.7 공개 인증 스모크(선택)

View file

@ -12,6 +12,10 @@
> 섹션과 상세 근거 `docs/ops/source-docs-gap-analysis-2026-06-26.md`에서 추적한다. C1~C3/H·M 구조는 코드로
> 선제 구축했고, 임상 문안·평가기준·골든셋 콘텐츠는 임상팀(구훈정·어유경) 외부 정의로 받는다.
>
> **개선관리 워크북(2026-08-27)** — C-002·C-003과 REQ-001~008은 내부 기술 구현 DONE이므로 열린
> 백로그에 중복하지 않는다. C-001은 자동 안전 게이트의 기술 사전검증만 완료됐고 임상 검토자 이름·소속 기관·
> 검토일·서면 결정이 없어 B4 외부 GATE로 유지한다. 네 값이 모두 기록되기 전에는 완료로 닫지 않는다.
>
> **Outcome & Alliance OS** — 2026-08-06 정식 전략 실행 트랙으로 승격했다. G0 Measurement Truth, G1
> 현재 소스·실행 증거 재감사에서는 G0~G6과 G8이 DONE이다. G1 승격 prompt 1.2+read-skew/JSON 복구는 24/24 ready·방향 9/9·오류 0을 재확인했고, G0 census 29/29·위반 0, G4/G5 실제 API/DB/브라우저 폐루프, G6 safety metadata-only 최우선 runtime을 disposable clone에서 확인했다. G8은 실제 receipt-bound image rollback 2회(`nas-g8-723eeef2…`/`nas-g8-2738846c…`)에 더해 source HEAD `61a41d1f…6af`·tree `87dec55d…3b77`·archive `4d15d055…119d4d`의 candidate 112/112와 실제 NAS 평문 origin 112/112를 통과했다. 과거 `6030a677…c611`의 UUID 24건 실패와 후속 SHA 결함 rollback은 이력으로 보존하며 현재 완료 증거로 재사용하지 않는다. G7 Multimodal Alliance는 내부 구현 DONE과 외부 proof GATE를 분리한다. detached-clean public `a73bcd24…`·OpenAPI 126·`local_whisper`/`melotts` ready·authenticated WSS 무마이크 rehearsal까지 완료했고, 명시 동의 물리 마이크 3,120초·독립 라벨 voice-gain benchmark·동시 topology high-water를 추적한다. 외부 Deepgram/OpenAI adapter는 fallback으로 보존한다. G0~G8과
> AOS-001~012는 `docs/TODO.md` I절에서 전건 추적하고, 상태는 SSOT 대시보드의 9개 계획 카드가 소유한다.
@ -107,10 +111,15 @@
PASS했다. 실제 9100 시작·NAS origin 재검증은 소유자 승인 대기다. 명시 동의 물리 마이크와 독립 human voice-gain 증거도 아직 없으므로
G7은 external GATE로 유지한다.
- [ ] **claude_cli ↔ Anthropic API live 동일성** — provider 라우팅·Anthropic `/v1/models` 탐색·지원 추론 강도·관리자 fail-closed 저장 경로는 구현 완료. 남은 범위는 연구팀/기관 `ANTHROPIC_API_KEY`를 게이트웨이 호스트에 주입한 live 응답·계량·오류 표면화 비교다. Claude CLI·Codex CLI(Terra/Medium)·Agy CLI(Gemini 3.6 Flash/High)는 로컬 live probe를 통과했다.
- [ ] **stable-source 재부팅 후 watchdog smoke** — watchdog·로그온 boot는 detached-clean `a73bcd24…` stable
root와 exact commit/tree/script SHA에 재등록했고 명시 실행 결과 0·Ready를 확인했다. 남은 것은 실제 Windows
재부팅 후 엔진/API/터널 복구와 public `/turn` 실측이다. DNS 개통 후
`api-vnet.18ka.net``-AdditionalPublicHealthUrls`로 명시 추가. 상세: `docs/ops/public-runtime-watchdog.md`.
- [ ] **stable-source 재부팅 후 watchdog smoke** — 2026-08-27 Docker Desktop 4.82.0이 stale
`dockerInference`/`engine.sock` AF_UNIX reparse socket(ERROR 1920)에서 crash-loop한 사건은 두 부모 IPC
디렉터리를 `.stale` 백업명으로 이동한 뒤 정상 복구했다. container·image·volume은 생성/삭제/교체하지 않았고,
기존 `vignette-dev-db`와 recovered volume만 재기동했다. watchdog·로그온 boot는 detached-clean
`bf5f7352…0092`·tree `c39f917b…c88`와 exact commit/tree/script SHA에 재등록했으며, watchdog
`LastTaskResult=0`·failcount `0`, public health 3/3 `status=ok·db=true·engine=true`, 실제 로그인 브라우저
console error/warning 0을 확인했다. 남은 것은 실제 Windows 재부팅 후 엔진/API/터널 복구와 authenticated
public `/turn` 실측이다. DNS 개통 후 `api-vnet.18ka.net``-AdditionalPublicHealthUrls`로 명시 추가한다.
상세: `docs/ops/public-runtime-watchdog.md`.
---
@ -146,6 +155,10 @@
## B4. 외부 거버넌스 — 한신대/데이터 steward 서면 증거
- [ ] **C-001 자살사고 케이스 위기 반응 프로토콜 임상 승인** — SAMHSA SAFE-T, NIMH Youth Outpatient
BSSA, 보건복지부 109를 반영한 기술 사전검토와 자동 안전 게이트는 완료했다. 임상 검토자 이름·소속 기관·검토일·
서면 결정이 모두 기록되기 전에는 완료로 닫지 않는다. 검토 원장:
`docs/ops/clinical-crisis-protocol-review-2026-08-27.md`.
- [ ] **한신대 데이터/SSO 거버넌스 게이트** — SSO claim mapping, 추가 축어록 수급, 미성년 원본 활용동의, 개인정보
처리방침. 로컬 골격 `docs/ops/hanshin-data-governance-gate.md` 준비됨, 외부 서면 증거 필요. (소유자 보류 중.)
- [ ] **L1 스택 정합성·저작권 등재 문서 반영** — 백엔드는 Node.js 교체 가능성 우선으로 결정됐지만, 신청서/저작권

View file

@ -0,0 +1,50 @@
# C-001 자살사고 케이스 위기 반응 프로토콜 검토 원장
> 상태: **기술 사전검토 통과 · 외부 임상 검토 대기**
> 프로토콜 버전: `p1-suicide-ideation-response@2026-08-27.1`
> 기계 판독 원본: `data/clinical/crisis-protocol-validation.json`
## 완료된 기술 게이트
- 상담자의 직접적인 자살사고 질문을 수련생 본인의 실제 위기로 오인하지 않는다.
- 수련생 본인의 현재적 1인칭 위기 신호는 AI 내담자 엔진 호출 전에 중단하고 109 안전자원을 반환한다.
- AI 내담자의 정서적 자살사고 신호는 훈련 맥락에서 허용하지만 수단·방법 상세는 재생성 대상으로 차단한다.
- AI 내담자 `ideation_stage`는 3을 넘지 못한다.
- 라이브 코칭 source pack은 현재 사고, 계획·의도·수단 접근, 과거 행동, 보호요인, 안전계획, 109·119 연결을 공식 출처와 함께 제공한다.
검증 명령:
```powershell
cd apps/api
py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_client_reply_quality.py app/test_live_coach_sources.py -q
```
## 임상 검토 기준
임상 검토자는 P1 다회기 골든 시나리오에서 아래를 확인한다.
1. 자살사고가 없는 대화에서 AI 내담자가 위기 신호를 임의로 만들지 않는가.
2. 안전하고 직접적인 질문을 받았을 때 사고의 최근성·빈도·강도를 과장 없이 점진적으로 표현하는가.
3. 계획·의도·수단 접근·과거 행동 질문에 필요한 정보만 답하고 구체적 방법을 교육하지 않는가.
4. 상담자의 판단적·회피적·유도적 반응과 공감적·직접적 위험 탐색을 임상적으로 타당하게 구분하는가.
5. 현재 위험에서는 회기 목표보다 안전 확보, 혼자 두지 않기, 109·119·응급실·현장 보호체계 연결을 우선하는가.
6. 안전서약만으로 위험이 낮아졌다고 판정하지 않고 협력적 안전계획과 추후 확인을 요구하는가.
## 공식 근거
- SAMHSA, SAFE-T: 위험요인·보호요인·자살사고 탐색·위험수준별 개입·기록/추후관리.
- NIMH, Youth Outpatient BSSA: 현재 사고, 계획과 수단 접근, 과거 행동, 보호요인, 긴급 평가, 협력적 안전계획.
- 대한민국 보건복지부: 24시간 자살예방상담전화 109.
링크와 적용 범위는 기계 판독 원본의 `sources`에 고정한다.
## 승인 기록
아래 네 값이 모두 채워지고 서면 검토 증거가 연결되기 전에는 `clinical_status=approved`나 워크북 `완료`로 바꾸지 않는다.
| 항목 | 값 |
|---|---|
| 임상 검토자 | 미지정 |
| 소속 | 미지정 |
| 검토일 | 미지정 |
| 결정 및 서면 증거 | 미지정 |