현재 작업 전체 반영

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

@ -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`)에서 페르소나 헬퍼가 실패하는 것은 환경 문제이지 코드가