개선관리 요구사항과 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

@ -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 공개 인증 스모크(선택)