런타임 계약과 학습자 흐름 보강

This commit is contained in:
Yun Chan 2026-06-29 08:12:14 +09:00
parent f456b8997a
commit 206018b088
56 changed files with 4306 additions and 1008 deletions

View file

@ -141,7 +141,9 @@ vignette/
| L6 | 직전 K턴 맥락(히스토리) + 이번 발화(L5) | ❌ | 상담자=user, 내담자(자기)=assistant 매핑 |
메시지 순서: `system(L0+L1, cache)``system(L2, cache)``system(L3)``system(L4)`
assistant/user 히스토리(L6) → `user(이번 마스킹 발화, L5)`. 게이트웨이는 마지막 user를 stdin으로 주입한다.
assistant/user 히스토리(L6) → `user(이번 마스킹 발화, L5)`. 현재 Python gateway의
`_split_messages()` 경계는 system 묶음과 마지막 user payload만 소비한다. L6의 system 외
history를 실제 프롬프트에 직렬화하는 변경은 별도 프롬프트 동작 패치로 다룬다.
**안전 불변식**(`L0_SAFETY`, docstring R4/R5/M6):
@ -215,7 +217,7 @@ RBAC×AIView로 차단된다. 이 모듈은 평가 신호만 산출한다.
supervisor rationale/critique + alternative_utterances. `_deep_schema()`.
- 정답 라벨 enum은 `app/taxonomy.py`가 단일 원천(SoT). LLM 출력은 enum으로 안전 파싱(미지값 폐기,
`_parse_technique`/`_parse_client_state`). 게이트웨이 structured 우선, 없으면 text에서 JSON 추출
(`_structured_payload`, 코드펜스 관용).
(`structured_payload_from_response()`, 코드펜스 관용).
- evaluator structured 결과는 `evaluate_turn()`/`evaluate_session()` 경계에서 canonical
`GenerateRequest` SHA-256 키의 인메모리 semantic cache로 재사용할 수 있다. `/admin/usage`
cache key·prompt·completion 없이 enabled/entries/hits/misses/stores/evictions/requests/hit_rate와
@ -426,13 +428,19 @@ GET {ENGINE_URL}/ready|/health — readiness/liveness
- 단일 프로세스(uvicorn) 가정의 단순 dict. 멀티워커에선 DB가 SoR이므로 무방.
- 영속/폴백 분기는 `app/runtime_policy.py`(`runtime_fallback_allowed` / `require_runtime_fallback_allowed`)가
게이트. 라우트는 `session_persistence`(DB) → 실패 시 store(in-proc) 순으로 시도한다.
- 회기 평가 저장은 `SessionEvaluationWrite``status/source/scope/stage/payload/error` write packet을
소유한다. `routes/sessions.py``routes/eval.py`는 같은 named packet을 만들어 저장하고, DB SQL과
fallback cache record는 `session_persistence.py`가 유지한다.
### 2.12 페르소나 카탈로그 — `app/persona_repository.py`
DB `app.persona_card`가 승인 페르소나의 SoR. 이 모듈이 in-proc `PersonaCard`와 DB 행을 잇는 경계다.
브라우저-facing persona catalog/review/draft/source/evidence DTO와 deterministic mapper는
`app/persona_read_model.py`가 소유한다. `routes/personas.py`는 route/auth, teacher/admin gate,
DB repository 호출, RAG source 등록, LLM draft generation, HTTP error mapping을 유지한다.
`app/persona_read_model.py`가 소유한다. draft generation structured schema, prompt bundle
id/version/hash, `GenerateResponse` payload extraction, generated draft default/coercion은
`app/persona_generation_contract.py`가 소유한다. `routes/personas.py`는 route/auth,
teacher/admin gate, DB repository 호출, RAG source 등록, engine invocation, provenance assembly,
HTTP error mapping을 유지한다.
- `load_file_personas()` / `built_in_personas()` — in-code P1~P3과 저장소 `data/personas/P4~P7.json`을 deterministic catalog로 합친다.
- `materialize_seed_personas()` — built-in P1~P7을 초기 승인 카탈로그로 누락분만 insert(admin 롤). `ON CONFLICT DO NOTHING`이므로 교수 편집본을 덮어쓰거나 `archived` 보관본을 재승인하지 않는다.
@ -476,7 +484,8 @@ cd apps/api && uvicorn engine_gateway.gateway:app --host 0.0.0.0 --port 9099
- **상주 풀** `/session`, `/session/{sid}/turn`, `/session/{sid}`(DELETE) — 회기 단위 프로세스.
- **stateless 어댑터**(engine_client 계약과 정합):
- `POST /v1/generate``EngineMessage[]``_split_messages()`로 (system_prompt, last_user) 분리
- `POST /v1/generate``EngineMessage[]``_split_messages()`
`GatewayPromptParts(system_prompt, user_payload)` 분리
`--system-prompt`로 주입, 마지막 user를 stdin content로. structured_schema는
`_inject_schema()`로 system에 JSON 준수 지시 주입(claude -p는 response_format 미지원이라 차선).
- `POST /v1/stream``turn_stream()`이 assistant 텍스트 델타를 즉시 yield → SSE
@ -488,9 +497,10 @@ cd apps/api && uvicorn engine_gateway.gateway:app --host 0.0.0.0 --port 9099
provider 라우팅 모드는 `ENGINE_MODE`(`app/config.py`)로 선택: `claude_api`(기본, Anthropic Messages API
직결) / `claude_cli`(로컬 상주 풀) / `openai`(폴백/평가 보조) / `solar`(국내, PII 민감구간 inference_geo:kr).
> 포트 주의: 게이트웨이 docstring 예시는 `:9099`이고, `app/config.py``engine_url` 기본값은
> compose 서비스명 기반 `http://engine:8100`이다. 로컬에서는 `ENGINE_URL`을 게이트웨이 실제 포트
> (예: `http://127.0.0.1:9099`)로 맞춰 주입한다.
> 포트 주의: 게이트웨이 docstring 예시는 `:9099`이고, `app/config.py`의 bare Settings fallback은
> legacy compose 서비스명 기반 `http://engine:8100`이다. 현재 지원 compose/local runtime은
> `ENGINE_URL`을 실제 host gateway 포트(예: `http://host.docker.internal:9099` 또는
> `http://127.0.0.1:9099`)로 override한다.
---

View file

@ -55,6 +55,21 @@ powershell -NoProfile -ExecutionPolicy Bypass -File scripts\start-tailscale-runt
- 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` 안내로 되돌린다.
### 0.2 Public runtime watchdog
공개 API 복구 스크립트는 `docs/ops/public-runtime-watchdog.md`가 runbook이다. 로컬 개발 서버와 별개로
prod API 8001, web preview 5174, engine gateway 9099, cloudflared tunnel을 검사한다.
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\watch-public-runtime.ps1 -CheckOnly
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\install-public-runtime-task.ps1 -RunNow
```
- Scheduled Task는 현재 Windows 사용자 기준 `AtLogOn` + 반복 watchdog이다. 사용자 로그인 전 headless boot service가 아니다.
- secret은 task 인자에 넣지 않는다. API secret은 `apps/api/.env`, cloudflared/Claude CLI credential은 사용자 profile에 둔다.
- 아직 DNS가 없는 future host는 기본 검사에 넣지 않는다. `api-vnet.18ka.net`처럼 실제로 열린 뒤에만 `-AdditionalPublicHealthUrls`로 명시 추가한다.
- 완료 판정은 parser/check-only가 아니라 실제 재부팅 또는 로그오프/로그온 뒤 `Get-ScheduledTaskInfo`, watchdog 로그의 `restart verified`, public health, 인증된 public `/turn` smoke까지 한 세트로 남겨야 한다.
### 수동 기동(대안)
DB·엔진 없이도 UI/로그인/페르소나/세션 생성까지는 동작한다(턴 생성만 불가).
@ -73,7 +88,7 @@ DB·엔진 없이도 UI/로그인/페르소나/세션 생성까지는 동작한
## 1. 사전 준비
- **Python 3.11** (운영 스크립트가 Python 3.11 기준). 가상환경 권장.
- **Node.js**(최신 LTS) + npm — `apps/web`.
- **Node.js 22 + npm** — CI와 같은 기준. newer LTS는 별도 재검증 전까지 기준선으로 쓰지 않는다.
- (선택) **Docker Desktop**`infra/docker-compose.yml` 전체 스택을 띄울 때만.
- (선택) **`claude` CLI** — `ENGINE_MODE=claude_cli`로 실제 턴 생성을 할 때. 설치 후 로그인되어 있어야 한다.
@ -163,7 +178,27 @@ cd D:\workspace\vignette
py -3.11 scripts\materialize-persona-seeds.py --json
```
### 2.4 DB 없이 degraded 기동 (정상 동작)
### 2.4 M2 session digest worker 실행
세션 종료 시 저장되는 fallback digest를 LLM 후보로 압축해 볼 때는 명시 session id runner를 쓴다.
기본은 dry-run이며, DB row를 바꾸려면 `--apply`를 반드시 붙인다. runner는 DB에서 작업을 읽은 뒤
connection을 놓고 engine을 호출하고, accepted 결과만 짧은 DB acquire로 적용한다.
```powershell
cd D:\workspace\vignette
py -3.11 scripts\run-session-digest-worker.py --session-id <session_uuid> --json
# accepted 후보를 실제 session_summary/case_profile에 반영할 때만
py -3.11 scripts\run-session-digest-worker.py --session-id <session_uuid> --apply --json
```
- 출력은 기본적으로 metadata-only다. digest 본문은 민감할 수 있으므로 `--show-digest`를 명시할 때만 출력한다.
- 입력은 client-visible `text_masked` transcript와 open thread만 사용한다. raw `text`, evaluator-only turn, CCD, `end_state`는 압축 prompt에 넣지 않는다.
- API 서버는 `SESSION_DIGEST_WORKER_ENABLED=true`일 때만 세션 종료 뒤 같은 worker를 background task로 실행한다. 기본값은 false다.
- scheduler도 DB load/apply 구간만 connection을 잡고, engine 호출은 DB transaction 밖에서 수행한다.
- 장시간 provider 운영, 임상 골든셋 품질평가, 재압축 정책은 별도 gate다.
### 2.5 DB 없이 degraded 기동 (정상 동작)
DB 연결이 안 되어도 dev에서는 그대로 기동한다. `main.py` lifespan이 풀 초기화 예외를 잡고
`store` 인메모리 폴백으로 degraded 기동하며, 다음 경고를 남긴다.
@ -432,8 +467,8 @@ RAG 임베딩/리랭커 의존성은 기본 이미지에 설치하지 않는다.
```powershell
# 백엔드 (apps/api)
cd apps\api
python -m pytest app/ -q # 현재 178 pass
python -m pytest engine_gateway\ -q # 현재 11 pass
python -m pytest app/ -q # 백엔드 기준선 178 pass
python -m pytest engine_gateway\ -q # 현재 27 pass
# 웹 (apps/web)
cd apps\web

View file

@ -60,7 +60,7 @@
| ID | 갭 | 현재상태 | 권고 | 근거 |
|---|---|---|---|---|
| **H1** | 계약 평가 KPI(자기효능감·기술숙련도·수련만족도 사전사후) 수집·입력·CSV/report 계산 2차 | `app.learner_prepost_measure`, 학습자 본인용 `GET/PUT /users/me/prepost-measures`, `SessionReview`의 파일럿 증거 원장 카드가 3척도 pre/post 1~5 aggregate evidence를 저장·조회한다. `app.services.phase3_kpi_export``scripts/export-phase3-kpi.py`는 원장 row를 Phase 3 evidence root의 `02-measures/prepost_measures.csv``02-measures/kpi_report.json` scaffold로 산출한다. participant id는 가명화하고, 3척도 paired normalized mean pre/post/delta, complete/missing pair를 계산한다. 2차에서는 `phase3_kpi_contract.py`가 KPI metric 이름·필수키·`computed_prepost`/`design_pending` status 값을 소유해 exporter/checker/test의 drift를 줄이고, 계산 가능한 pre/post evidence와 평가설계 전 미계산 KPI를 report 안에서 구분한다. | 공식 문항 확정, 실험/통제군 배정, 추이 시각화, 통계검정 종류/alpha/결측 처리, 실제 20명 evidence와 steward/legal/IAA 검수. 현재 API/UI/export는 공식 효과성·성적·수료 판정이 아니라 파일럿 evidence 계산이다. (κ/ICC·환각률은 doc4 미명시 → 평가설계 확정.) | doc4(20명 실험/통제군·단회기 50분·3척도 pre-post) |
| **H2** | 턴별 fast-loop + 라이브 코칭 1차 가동·학습자 리뷰 2열 UI 1차 완료·골든셋 잔여 | `make_eval_hook`이 submit/voice 생성 경로에 주입되고, stream은 `_evaluate_stream_turn`으로 fast-loop 평가를 붙인다. 결과는 `feedback_scores`, `alternative_utterance` 등 정규화 테이블에 적재·hydrate된다. 추가로 `app/services/live_coach.py`, `POST/GET /sessions/{id}/live-coach`, `POST /kb/live-coach/source-packs/sync`, `app.live_coach_events`, `data/kb/live_coaching_workbook_0615.json`, `data/kb/live_coaching_sources/*.json`을 연결해 워크북·DSM·공식 지침 요약 기반 코칭 아바타 말풍선·근거 모달·발화별 이력 오버레이와 RAG 증분 색인을 제공한다. `app.services.source_pack_sync`가 repo source pack의 active `content_hash`를 비교하고 변경 시 document version을 최신+1로 올린다. live turn은 프로세스 로컬 source pack snapshot을 재사용하지만, 관리자 sync/CLI는 `refresh=True`로 캐시를 비운 뒤 repo 파일을 다시 읽어 stale `content_hash` 비교를 막는다. 학습자 `SessionReview` 데스크톱은 좌측 축어록 타임라인, 우측 요약·감정·흐름·루브릭·강점·개선점·pre/post·워크시트·피드백 작업열의 2열 구조로 재배치했다. | 원천 축어록 few-shot 골든셋 적재, 임상팀 확정 루브릭과 source pack 임상 검수 상태 운영정책 보강. | doc2·doc5(골드 포맷) |
| **H2** | 턴별 fast-loop + 라이브 코칭 1차 가동·학습자 리뷰 3열 workbench 완료·골든셋 잔여 | `make_eval_hook`이 submit/voice 생성 경로에 주입되고, stream은 `_evaluate_stream_turn`으로 fast-loop 평가를 붙인다. 결과는 `feedback_scores`, `alternative_utterance` 등 정규화 테이블에 적재·hydrate된다. 추가로 `app/services/live_coach.py`, `POST/GET /sessions/{id}/live-coach`, `POST /kb/live-coach/source-packs/sync`, `app.live_coach_events`, `data/kb/live_coaching_workbook_0615.json`, `data/kb/live_coaching_sources/*.json`을 연결해 워크북·DSM·공식 지침 요약 기반 코칭 아바타 말풍선·근거 모달·발화별 이력 오버레이와 RAG 증분 색인을 제공한다. `app.services.source_pack_sync`가 repo source pack의 active `content_hash`를 비교하고 변경 시 document version을 최신+1로 올린다. live turn은 프로세스 로컬 source pack snapshot을 재사용하지만, 관리자 sync/CLI는 `refresh=True`로 캐시를 비운 뒤 repo 파일을 다시 읽어 stale `content_hash` 비교를 막는다. 학습자 `SessionReview` 데스크톱은 요약/흐름, 축어록, 평가 rail의 3열 workbench와 하단 워크시트로 재배치했고, empty review는 2열 이하로 유지한다. 워크시트/pre-post/교수자 메모 입력과 발화 이동 버튼은 name/autocomplete/aria-label 및 공유 focus token으로 접근성 회귀 표면을 줄였다. | 원천 축어록 few-shot 골든셋 적재, 임상팀 확정 루브릭과 source pack 임상 검수 상태 운영정책 보강. | doc2·doc5(골드 포맷) |
| **H3** | 임상팀 콘텐츠 입력 경로(페르소나 저작 CRUD) 2차 구현·원문 격리 정책 1차·항목형 목록 저작/프롬프트 검토 UI 완료·임상 검수 잔여 | draft 생성·조회·편집·검수요청 API와 교수 콘솔 JSON 초안 패널은 연결됐다. `persona_repository.py`는 in-code `SEED_PERSONAS`(P1~P3)와 `data/personas/P4.json`~`P7.json``PersonaCard`로 합쳐 `materialize_seed_personas()`와 seed fallback catalog에 포함한다. `scripts/materialize-persona-seeds.py`는 같은 seed manifest를 dry-run 기본으로 보고하고, `--apply`에서만 DB pool을 초기화한 뒤 기존 idempotent DB materializer를 호출한다. `scripts/sync-persona-sources.py`는 DB-backed dry-run/apply runner로 repo-managed source pack의 `content_hash`/document version을 비교한다. RAG 기반 draft 생성은 source/chunk evidence와 함께 `persona-draft-rag@2026-06-28.1` prompt bundle id/version/hash를 engine metadata 및 draft `source_provenance`에 남긴다. `POST /personas/sources`는 raw 원문 hash-only 증거를 `kb.raw_source_artifact`에 따로 기록하고, sanitized 파생본만 evaluator-only RAG chunk로 색인한다. `rag.index_document()``sensitivity=3` 또는 raw marker chunk를 DB 접근 전에 차단한다. `app/persona_read_model.py`는 catalog/review/draft/source/evidence DTO와 mapper를 route에서 분리해 OpenAPI schema 이름을 유지한다. PersonaStudio는 자동사고, 회기 시나리오, 말투 filler/verbal tic/nonverbal cue, 역린·금기 응답·금기어를 행 추가/삭제 UI로 편집하고, 저장 직전 빈 항목을 제거하되 기존 배열 schema를 유지한다. 프롬프트 탭은 raw JSON textarea 대신 L1 카드·인적 범주·임상 배경·말투·수치 파라미터·역린·회기 시나리오·추가 계약 섹션으로 같은 draft 데이터를 검토하게 한다. | 루브릭·이론 콘텐츠 외부화, P4~P7 포함 임상팀 최종 검수/서면 evidence 확보, 암호화 blob/vault 기반 원문 실저장. | doc3(R&R)·doc4(페르소나=전문가 산출물) |
| **H4** | PII 마스킹 한국어 로컬 휴리스틱 + optional ko recognizer adapter + 15-case fixture/schema 평가 harness + 온보딩·동의 게이트 잔여 | Presidio `language='en'` 고정 한계를 보완하기 위해 정규식 폴백에 한국어 날짜·금액·행정구역 주소와 함께 이름/성명 라벨, 성씨+이름+조사/호칭, 대학교·학과·병원·센터 등 기관 suffix 기반 로컬 휴리스틱 마스킹을 추가했다. 66차에서는 `제 이름은 김서연입니다`, `보호자 이름은 박민수입니다`, `저는 최하늘입니다` 같은 자연 발화형 이름 라벨·자기소개 패턴을 추가하고 `이름은 중요하지 않다` negative control로 오탐을 막았다. Presidio가 설치돼도 한국어 누락을 막기 위해 fallback을 후단에 한 번 더 태운다. 70차에서는 `guardrail.mask_pii()` 내부에 선택형 한국어 PII recognizer adapter 경계를 추가했다. adapter는 import-time hard dependency가 아니며 명시 등록 전에는 비활성이고, 실패해도 기존 regex fallback이 마지막 안전망으로 유지된다. fake adapter 테스트는 regex가 못 잡는 별명/기관 span을 `[NAME]`/`[ORG]`로 마스킹하고 같은 문장의 전화번호는 후단 regex가 `[PHONE]`으로 처리하는지, adapter 실패 시에도 fallback이 유지되는지 검증한다. 상담 생성(generate/stream), fast evaluator prompt, client turn `text_masked`에서 한국어 NAME/ORG raw 값이 남지 않도록 회귀화했다. `app.services.pii_masking_eval`, `data/privacy/pii-masking-ko-fixtures.json`, `scripts/evaluate-pii-masking.py`로 합성 fixture 15케이스를 NAME/ORG/PHONE/EMAIL/RRN/NUMID/DATE/MONEY/ADDR/negative-control 범위에서 entity recall·forbidden substring removal·unexpected entity violation으로 평가한다. `data/privacy/pii-masking-eval-input.schema.json``data/privacy/pii-masking-eval-report.schema.json`은 source/category/severity metadata와 summary-only `technical_dry_run` 리포트 계약을 고정하며, 기본 CLI JSON은 `masked_text`/`forbidden_remaining` 원문 증거를 제외한다. `소속`/`안내`/`이름` NAME 오탐도 stopword로 보정했다. 외부 LLM 호출은 상담 생성(generate/stream)·fast/deep 평가 직후 `audit.llm_call_log`에 provider/model/token/cost/inference_geo/latency만 적재하도록 연결했고, prompt/completion 본문은 저장하지 않는다. 로컬 dev-login 실제 `/turn` smoke에서 `audit.llm_call_log` 3행 증가를 확인했다. `app_user`에 이름·소속·학과·학년/직위·연락처·주소/수령지·닉네임·자기소개·아바타 URL·약관/개인정보 동의 버전 필드를 추가했고, 로그인 직후 `/onboarding` 완료 전에는 역할 홈과 learner 회기 시작을 막는다. 아바타 이미지는 `/users/me/avatar`에서 MIME/시그니처/3MB 제한 후 파일 저장소에 두고 URL만 보관한다. 온보딩 저장 시 learner `consent_at`도 함께 세팅하며 auth E2E에서 신규 계정 온보딩→아바타 업로드→학습자 홈 이동을 검증했다. | 실제 ko recognizer 모델/provider 선정, 운영 말뭉치 기반 오탐/미탐 평가, 미성년/guardian 및 법무 검토가 필요한 최종 서명 동의서·개인정보 처리방침·약관 evidence 확보. 공개 Google OAuth 실제 `/turn` proof는 별도 운영 게이트. | doc1/2/5(실명·날짜·미성년·자살시도 다수)·doc4(IRB·개인정보) |
@ -69,11 +69,11 @@
| ID | 갭 | 현재상태 | 권고 |
|---|---|---|---|
| **M1** | 비언어/준언어 임상 이벤트 캡처·태깅 4차 진행 | 1차에서 `audio_ref`/`silence_ms`/`speech_rate`/`barge_in`을 learner voice turn에 저장하고 리뷰 `nonverbal` 칩으로 파생했다. 2차에서는 `app.turns.provider_events JSONB``TurnRecord.provider_events`를 추가해 WebSocket control/STT provider 이벤트를 allowlist·size limit 후 보존한다. 3차에서는 저장 전 sanitizer에서 내부 taxonomy `event_type`/`category`를 붙인다. 4차에서는 인증된 회기 리뷰 API가 `sigh`/`cry`/`laugh`/`breath`, prosody, background noise 계열만 한글 label/detail 칩으로 파생 노출한다. raw transcript/text payload, provider/source/raw type, 공개 공유 카드 노출은 제외한다. | 실제 provider 기반 한숨·울음·억양 감지 연결, 장시간 마이크/WSS 실측, 리뷰 칩을 역량 지표로 해석할지에 대한 정책. |
| **M2** | 다회기 종단 케이스 아크·교차회기 사례개념화 8차 구동 | `(persona_id, learner_id)` 안정 `case_profile` upsert, 원자적 `session_no`, voice/REST/SSE recall cache 주입을 연결했다. 세션 종료 시 마스킹 축어록 기반 fallback `session_summary.digest`, `case_profile.case_digest`, `rapport_trajectory`, `alliance_level`을 갱신하고, 다음 회기 seed recall은 `case_digest`·직전 `session_summary`·client-visible non-contradicted `pinned_fact`를 함께 조립한다. 3차에서는 마스킹된 client-visible 발화에서 `[NAME]`/`[ORG]` identity와 명시적 상담 약속만 보수적으로 `pinned_fact`에 upsert한다. 4차에서는 pinned fact 삽입 또는 값 변경 시 `pinned_fact_history`에 append-only 이력을 남긴다. 5차에서는 명시적 상담 약속 철회/부정만 기존 non-locked `agreement:counseling` fact를 `contradicted`로 격리하고 history reason `contradiction`을 남긴다. 6차에서는 세션 종료 저장 성공 뒤 마스킹된 client-visible 내담자 발화만 `app.turn_embedding`에 BGE-M3 dense/sparse로 idempotent 색인한다. 7차에서는 REST submit/SSE stream의 턴 컨텍스트 조립 경계를 `_prepare_turn_context()`로 묶고, DB seed recall과 pinned fact가 다음 턴 EngineMessage L2/L4에 raw 이름 마스킹 상태로 주입되는 route-level 회귀를 추가했다. 8차에서는 `SessionDigestInput`/`SessionDigestResult`/`SessionSummaryWrite`로 종료 digest 입력·fallback 결과·DB write 인자 경계를 명시해 future LLM worker가 raw text, evaluator-only turn, 평가 payload, CCD, deterministic carry를 압축 prompt에 우회 주입하지 못하게 했다. `digest_pending`은 CompressionJob 생성 여부를 알리는 비동기 압축 필요 신호로 유지한다. 같은 값 재확인은 history를 늘리지 않고, `locked` fact는 건드리지 않는다. 관계갈등·위기·임상 추론은 자동 pinning/모순 처리에서 제외한다. | 관계·임상 fact 승격 기준, LLM digest worker 실행/품질평가/재압축, 접수면접→다회기 연속성·자기개념 진화 실증. |
| **M2** | 다회기 종단 케이스 아크·교차회기 사례개념화 11차 구동 | `(persona_id, learner_id)` 안정 `case_profile` upsert, 원자적 `session_no`, voice/REST/SSE recall cache 주입을 연결했다. 세션 종료 시 마스킹 축어록 기반 fallback `session_summary.digest`, `case_profile.case_digest`, `rapport_trajectory`, `alliance_level`을 갱신하고, 다음 회기 seed recall은 `case_digest`·직전 `session_summary`·client-visible non-contradicted `pinned_fact`를 함께 조립한다. 3차에서는 마스킹된 client-visible 발화에서 `[NAME]`/`[ORG]` identity와 명시적 상담 약속만 보수적으로 `pinned_fact`에 upsert한다. 4차에서는 pinned fact 삽입 또는 값 변경 시 `pinned_fact_history`에 append-only 이력을 남긴다. 5차에서는 명시적 상담 약속 철회/부정만 기존 non-locked `agreement:counseling` fact를 `contradicted`로 격리하고 history reason `contradiction`을 남긴다. 6차에서는 세션 종료 저장 성공 뒤 마스킹된 client-visible 내담자 발화만 `app.turn_embedding`에 BGE-M3 dense/sparse로 idempotent 색인한다. 7차에서는 REST submit/SSE stream의 턴 컨텍스트 조립 경계를 `_prepare_turn_context()`로 묶고, DB seed recall과 pinned fact가 다음 턴 EngineMessage L2/L4에 raw 이름 마스킹 상태로 주입되는 route-level 회귀를 추가했다. 8차에서는 `SessionDigestInput`/`SessionDigestResult`/`SessionSummaryWrite`로 종료 digest 입력·fallback 결과·DB write 인자 경계를 명시해 LLM worker 후보가 raw text, evaluator-only turn, 평가 payload, CCD, deterministic carry를 압축 prompt에 우회 주입하지 못하게 했다. 9차에서는 `DigestQualityAssessment`/`SessionDigestWorkerOutcome`로 local quality harness를 추가해 빈/짧은 digest, raw forbidden substring, 내부 평가·CCD·상태 marker, 잘못된 `S{session_no}:` prefix를 fallback 유지 대상으로 판정한다. 10차에서는 `session_digest_worker.py``CompressionJob`을 Node-compatible `GenerateRequest`/`EngineMessage`로 변환하고, 주입형 engine/audit 호출 뒤 quality gate 통과 결과만 `session_summary.digest/compressed_by/token_count``case_profile.case_digest`에 적용하는 one-shot 경계를 소유한다. 11차에서는 `scripts/run-session-digest-worker.py` dry-run/apply runner와 `SESSION_DIGEST_WORKER_ENABLED=false` 기본값의 세션 종료 background scheduler 골격을 붙였다. scheduler는 DB load/apply 때만 connection을 잡고 engine 호출은 transaction 밖에서 수행하며, `compressed_by IS NULL` loader/apply CAS로 재실행 race를 막는다. DB loader는 persisted fallback summary와 client-visible `text_masked` transcript만 재구성하며 raw `text`, evaluator-only turn, CCD, end_state는 prompt에 넣지 않는다. `digest_pending`은 CompressionJob 생성 여부를 알리는 비동기 압축 필요 신호로 유지한다. 같은 값 재확인은 history를 늘리지 않고, `locked` fact는 건드리지 않는다. 관계갈등·위기·임상 추론은 자동 pinning/모순 처리에서 제외한다. | 관계·임상 fact 승격 기준, 실 provider 장시간 운영, 임상 골든셋 품질평가, 재압축, 접수면접→다회기 연속성·자기개념 진화 실증. |
| **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 비용 관측·예산 경고·평가 저비용 라우팅·evaluator cache 관측·일별 비용 추이·모델별 비용 검증 리포트 2차 완료 | 턴별 provider/model/tokens/cost 저장 경로와 `GET /admin/usage`, 관리자 비용 대시보드를 연결했다. `ADMIN_USAGE_BUDGET_USD` 기준 예산 상태(ok/warn/exceeded)도 응답/UI에 표시한다. `EVALUATOR_FAST_MODEL`/`EVALUATOR_DEEP_MODEL` 설정 시 fast/deep 평가 호출만 해당 모델 override로 gateway에 전달하고, 비워두면 기존 gateway default 라우팅을 유지한다. fast/deep evaluator structured 결과는 canonical request SHA-256 기반 인메모리 semantic cache로 재사용하며, 원문 prompt·completion은 저장하지 않고 성공 파싱 결과만 TTL/entry 제한 안에서 캐시한다. `/admin/usage`와 관리자 비용 카드가 cache enabled/entries/hits/misses/stores/evictions/requests/hit_rate와 일별 `daily_cost` 추이를 노출한다. `app.services.usage_report``scripts/report-ai-usage.py`는 같은 usage JSON에서 provider/model별 cost share, token share, cost/turn, cost/1k tokens, metered coverage, budget/cache warning을 산출한다. DB 미가용 dev는 runtime store fallback, prod는 fail-closed다. | 자동 차단·한도 enforcement 정책. |
| **L1** | 기술스택 신청서-구현 불일치 및 단기일정 산출물 압박 | doc4 신청서 스택(Spring Boot 3/Node.js·TimescaleDB) vs 실제 FastAPI/Python 불일치, 20주 단기일정·9월 저작권 등재 압박. 소유자 결정으로 장기 교체 대상은 Node.js 우선, 현재 FastAPI 전면 재작성은 보류했다. 내부 전환 증거로 engine gateway 공유 계약, `EngineClient.stream_packets()` decode 경계, schema-backed golden fixture(`engine_gateway_contract.v1.json`/`engine_gateway_schema.v1.json`), Python import 없는 `scripts/check-engine-gateway-contract.mjs` Node.js conformance runner, 브라우저-facing 세션 read-model 분리(`app/session_read_model.py`), 페르소나 DTO/mapper 분리(`app/persona_read_model.py`), stage 라벨/phase-key 정규화 SSOT(`app/stage_contract.py`)까지 고정했다. | 신청서/저작권 등재 문서에 FastAPI 유지 사유와 계약 우선 Node 전환 계획을 반영하는 외부 거버넌스 증거. 내부 후보였던 H3 항목형 목록 저작 UI와 프롬프트 미리보기 de-JSON은 10차에서 완료. |
| **L1** | 기술스택 신청서-구현 불일치 및 단기일정 산출물 압박 | doc4 신청서 스택(Spring Boot 3/Node.js·TimescaleDB) vs 실제 FastAPI/Python 불일치, 20주 단기일정·9월 저작권 등재 압박. 소유자 결정으로 장기 교체 대상은 Node.js 우선, 현재 FastAPI 전면 재작성은 보류했다. 내부 전환 증거로 engine gateway 공유 계약, `EngineClient.stream_packets()` decode 경계, schema-backed golden fixture(`engine_gateway_contract.v1.json`/`engine_gateway_schema.v1.json`), Python import 없는 `scripts/check-engine-gateway-contract.mjs` Node.js conformance runner, `gateway-default` default-routing sentinel 정규화, `structured_payload_from_response()` 기반 structured/legacy JSON response parser, 브라우저-facing 세션 read-model 분리(`app/session_read_model.py`), 페르소나 DTO/mapper 분리(`app/persona_read_model.py`), 페르소나 draft generation 계약(`app/persona_generation_contract.py`), session evaluation write packet(`SessionEvaluationWrite`), stage 라벨/phase-key 정규화 SSOT(`app/stage_contract.py`)까지 고정했다. | 신청서/저작권 등재 문서에 FastAPI 유지 사유와 계약 우선 Node 전환 계획을 반영하는 외부 거버넌스 증거. 내부 후보였던 H3 항목형 목록 저작 UI와 프롬프트 미리보기 de-JSON은 10차에서 완료. |
> X2 근거: doc3 회의록이 'AI API 비용'을 운영 리스크로 명시.
@ -88,9 +88,10 @@
- **C1 4차 완료**: `caseWorksheet` 응답 구조, 리뷰 화면 편집 UI, `app.case_worksheet` 저장/재조회 경로, 외부 루브릭 schema/loader/validation scaffold, 교수자 수동 워크시트 검수 상태 저장/표시. 워크시트 템플릿 key source는 `CASE_WORKSHEET_SECTION_SPECS`/`case_worksheet_template_item_keys()`로 명시했다. 후속은 임상팀 확정 루브릭 콘텐츠, AI 채점 보정, 승인 후 잠금·재제출 정책.
- **C2 1차 완료**: 위기 신호는 엔진 전 중단, 109 안내, `safety_events` 적재, 교수자 알림 큐, `ideation_observed` 전달까지 배선했다. 후속은 임상 스크립트·서약 문안·감점 루브릭.
- **C3 2차 완료**: `theory_mode`가 세션·평가·생성 프롬프트까지 흐르고, 학습자는 세션 시작 전 기존 3개 모드 중 하나를 명시 선택해 `POST /sessions`로 보낸다. 후속은 임상팀 CBT 체인·이론부합 루브릭.
- **H2 1차+라이브 코칭+리뷰 2열 UI 완료**: `make_eval_hook`과 stream 평가가 턴 파이프라인에 붙고 정규화 테이블로 적재·복원된다. 대안발화도 `app.alternative_utterance`로 정규화한다. 라이브 코칭은 0615 워크북·DSM·공식 지침 요약/RAG 근거로 코칭 아바타 말풍선·근거 모달·발화별 이력 오버레이까지 연결했다. `POST /kb/live-coach/source-packs/sync`는 공용 source pack sync service를 통해 evaluator 전용 RAG에 증분 색인하고, hash 변경 시 document version을 최신+1로 올린다. 관리자 sync/CLI는 source pack manifest 생성 전 process-local cache를 refresh해 현재 repo JSON 기준으로 비교한다. 학습자 `SessionReview` 데스크톱은 좌측 축어록 타임라인과 우측 작업열의 2열 구조로 재배치했고, 교수자/모바일 레이아웃은 기존 규칙을 유지한다. 후속은 골든셋·임상팀 루브릭·source pack 임상 검수 상태 운영.
- **H2 1차+라이브 코칭+리뷰 3열 workbench 완료**: `make_eval_hook`과 stream 평가가 턴 파이프라인에 붙고 정규화 테이블로 적재·복원된다. 대안발화도 `app.alternative_utterance`로 정규화한다. 라이브 코칭은 0615 워크북·DSM·공식 지침 요약/RAG 근거로 코칭 아바타 말풍선·근거 모달·발화별 이력 오버레이까지 연결했다. `POST /kb/live-coach/source-packs/sync`는 공용 source pack sync service를 통해 evaluator 전용 RAG에 증분 색인하고, hash 변경 시 document version을 최신+1로 올린다. 관리자 sync/CLI는 source pack manifest 생성 전 process-local cache를 refresh해 현재 repo JSON 기준으로 비교한다. 학습자 `SessionReview` 데스크톱은 요약/흐름, 축어록, 평가 rail의 3열 workbench와 하단 워크시트로 재배치했고, 교수자/모바일 레이아웃은 기존 규칙을 유지한다. 후속은 골든셋·임상팀 루브릭·source pack 임상 검수 상태 운영.
- **H3 10차 완료**: 페르소나 저작 CRUD(draft→review), P4~P7 저장소 JSON 로드, RAG source 기반 draft generation, prompt bundle id/version/hash provenance와 seed/version materializer runner가 붙었다. seed runner는 dry-run/JSON manifest를 제공하고, `--apply`에서만 DB pool을 초기화해 기존 materializer를 호출한다. repo-managed source pack runner는 DB-backed dry-run/apply를 제공하며 active `content_hash`가 바뀐 문서만 최신 version+1로 sync한다. `kb.raw_source_artifact` hash-only 레코드와 `rag.index_document()` raw/sensitivity=3 fail-closed guard를 추가해 raw 원문이 `kb.chunk`/embedding/FTS로 들어가지 않게 했고, `app/persona_read_model.py`로 persona DTO/mapper 경계를 분리했다. PersonaStudio의 자동사고·회기 시나리오·말투 목록·역린/금기 목록은 행 추가/삭제 UI로 바꾸고 기존 배열 payload 계약을 유지한다. 이번 패스에서 프롬프트 미리보기 raw JSON textarea를 라벨형 검토 섹션으로 교체했다. 후속은 루브릭·이론 콘텐츠 외부화, 임상팀 최종 검수 evidence, 암호화 blob/vault 기반 원문 실저장.
- **H4(부분)·X1·X2**: 한국어 날짜/금액/주소 및 이름/기관 로컬 휴리스틱 마스킹, 15-case 합성 fixture 평가 harness와 input/report schema(summary-only `technical_dry_run` report), 자연 발화형 이름 라벨·자기소개 마스킹 보강, optional ko recognizer adapter 인터페이스와 fake/failure 회귀, 외부 LLM 호출 metadata-only `audit.llm_call_log` 적재 경로, 학습자 동의 수락/철회/회기 시작 하드게이트 골격은 완료했다. 실제 ko recognizer 모델/provider 선정, 운영 말뭉치 기반 평가, guardian/legal 서명 evidence는 후속이다. dry-run JSONL export·PII scan·IAA 계산 1차도 완료했고 `ds.*` write는 `--write-dataset` 명시 시에만 수행한다. X2 비용 관측·예산 경고, evaluator fast/deep 모델 override, evaluator semantic cache, 운영 hit-rate 관측, 일별 비용 추이, 모델별 비용 검증 리포트는 완료했고 자동 차단·한도 enforcement 정책은 후속. M1은 provider_events 보존 슬롯, 내부 taxonomy, 인증 리뷰용 제한 파생 칩까지 완료했고, M2는 보수적 identity/agreement pinned_fact 자동 실적재, append-only history, 명시적 상담 약속 철회 contradiction, episodic embedding writer, 다음 턴 EngineMessage 주입 회귀까지 완료했다. L1은 Node.js conformance runner와 session/persona read-model 분리까지 완료했다. 실제 provider 기반 한숨·울음 감지는 후속.
- **H3 11차 / L1 계약 보강 완료**: `app/persona_generation_contract.py`가 페르소나 draft structured schema, prompt bundle id/version/hash, `GenerateResponse` payload extraction, generated draft defaults/coercion을 소유한다. `routes/personas.py`는 auth, RAG evidence, engine invocation, provenance assembly, HTTP error mapping을 유지한다. 이어서 evaluator/live-coach의 로컬 structured payload alias를 제거하고 `structured_payload_from_response()`를 직접 호출하게 했으며, `SessionEvaluationWrite``app.session_evaluation` 저장 packet을 소유한다. 최신 L1 검증은 persona contract+review 39 passed, gateway contract 27 passed, backend focused 120 passed, Node conformance OK, `npm run check:api-types`, `npm run typecheck`.
- **H4(부분)·X1·X2**: 한국어 날짜/금액/주소 및 이름/기관 로컬 휴리스틱 마스킹, 15-case 합성 fixture 평가 harness와 input/report schema(summary-only `technical_dry_run` report), 자연 발화형 이름 라벨·자기소개 마스킹 보강, optional ko recognizer adapter 인터페이스와 fake/failure 회귀, 외부 LLM 호출 metadata-only `audit.llm_call_log` 적재 경로, 학습자 동의 수락/철회/회기 시작 하드게이트 골격은 완료했다. 실제 ko recognizer 모델/provider 선정, 운영 말뭉치 기반 평가, guardian/legal 서명 evidence는 후속이다. dry-run JSONL export·PII scan·IAA 계산 1차도 완료했고 `ds.*` write는 `--write-dataset` 명시 시에만 수행한다. X2 비용 관측·예산 경고, evaluator fast/deep 모델 override, evaluator semantic cache, 운영 hit-rate 관측, 일별 비용 추이, 모델별 비용 검증 리포트는 완료했고 자동 차단·한도 enforcement 정책은 후속. M1은 provider_events 보존 슬롯, 내부 taxonomy, 인증 리뷰용 제한 파생 칩까지 완료했고, M2는 보수적 identity/agreement pinned_fact 자동 실적재, append-only history, 명시적 상담 약속 철회 contradiction, episodic embedding writer, 다음 턴 EngineMessage 주입 회귀, digest contract/local quality harness, one-shot worker/runner/default-off scheduler/CAS 경계까지 완료했다. L1은 Node.js conformance runner, `gateway-default` default-routing sentinel 정규화, session/persona read-model 분리까지 완료했다. 실제 provider 기반 한숨·울음 감지는 후속.
### B. 소유자 결정 / 외부(임상팀·기관) 의존
@ -135,8 +136,8 @@ rg -n "materialize_seed_personas|init_pool|close_pool|--apply|--json" scripts/ma
```powershell
# 백엔드 (작업 디렉터리 apps/api)
python -m pytest app/ -q # 현재 118 pass
python -m pytest engine_gateway/ -q # 현재 11 pass
python -m pytest app/ -q # 백엔드 기준선 178 pass
python -m pytest engine_gateway/ -q # 게이트웨이 현재 27 pass
# 프론트 (작업 디렉터리 apps/web)
npm run typecheck
@ -154,13 +155,17 @@ npm run e2e # Playwright — web+api+DB 스택 필요
- OCR 잔재·오탈자 존재(doc1·doc5). 의미는 시각 보정했으나 일부 표현 불확실.
- **doc4 κ/ICC·환각률은 신청서 본문 미명시** — 계약 확정 지표로 단정 금지.
- doc4/doc3 행정 불일치(참여교수 1명 vs 2명, 서식 연도 '2025' 오기, 연구책임자 표기 불일치, 트웬티온스 성명 공란).
- '현재상태'는 grep/코드 사실 기반 적대적 비평 인용에서 시작했으나, **C1·C2·C3·H1 pre/post 원장·KPI export·metric status contract·H2·H3·M1 provider_events 보존/taxonomy/인증 리뷰 파생 칩·M2 case memory/pinned_fact/history/명시철회 contradiction/episodic embedding writer/다음턴 주입회귀/digest contract-only·M3, H4 로컬 마스킹/optional ko adapter/fixture 평가/감사/온보딩 경로, X1 dry-run export, X2 비용 관측·evaluator routing/cache/hit-rate 관측·모델별 비용 리포트, L1 engine gateway contract/golden/schema/Node conformance runner/session/persona read-model 분리는 구현 후 재검증 완료** 기준이다. H1은 파일럿 evidence 계산과 report 계약까지이며, 공식 효과성 판정·통계해석·20명 evidence는 완료로 보지 않는다. M2는 fallback digest`digest_pending` 응답 계약까지이며, LLM digest worker 실행·품질평가·재압축은 완료로 보지 않는다. 다만 H4의 실제 ko recognizer 모델/provider 선정과 운영 corpus 평가, guardian/legal evidence, 공개 OAuth `/turn` proof, M1 실제 provider 기반 한숨·울음 감지는 여전히 게이트로 남아 있다. L1 외부 거버넌스는 FastAPI 유지 사유와 Node 전환 계획의 제출/등재 증거가 남아 있다.
- '현재상태'는 grep/코드 사실 기반 적대적 비평 인용에서 시작했으나, **C1·C2·C3·H1 pre/post 원장·KPI export·metric status contract·H2·H3·M1 provider_events 보존/taxonomy/인증 리뷰 파생 칩·M2 case memory/pinned_fact/history/명시철회 contradiction/episodic embedding writer/다음턴 주입회귀/digest contract/local quality harness/one-shot worker+runner+default-off scheduler boundary·M3, H4 로컬 마스킹/optional ko adapter/fixture 평가/감사/온보딩 경로, X1 dry-run export, X2 비용 관측·evaluator routing/cache/hit-rate 관측·모델별 비용 리포트, L1 engine gateway contract/golden/schema/Node conformance runner/default-routing sentinel/structured payload parser/session read-model/persona read-model/persona generation contract/session evaluation write packet 분리는 구현 후 재검증 완료** 기준이다. H1은 파일럿 evidence 계산과 report 계약까지이며, 공식 효과성 판정·통계해석·20명 evidence는 완료로 보지 않는다. M2는 fallback digest, `digest_pending` 응답 계약, LLM 후보 local quality harness, accepted-only one-shot worker/runner/default-off scheduler 경계까지이며, 실 provider 장시간 운영·임상 골든셋 품질평가·재압축은 완료로 보지 않는다. 다만 H4의 실제 ko recognizer 모델/provider 선정과 운영 corpus 평가, guardian/legal evidence, 공개 OAuth `/turn` proof, M1 실제 provider 기반 한숨·울음 감지는 여전히 게이트로 남아 있다. L1 외부 거버넌스는 FastAPI 유지 사유와 Node 전환 계획의 제출/등재 증거가 남아 있다.
---
### 변경 이력
- 2026-06-28: M2 8차 digest contract-only 경계와 focused 74 passed 검증 기준 반영.
- 2026-06-29: L1 `gateway-default` default-routing sentinel, `structured_payload_from_response()`, persona generation contract, `SessionEvaluationWrite`와 gateway contract 검증 기준 반영.
- 2026-06-29: `scripts/check-dev-dashboard-ssot.py` dashboard SSOT drift gate와 M2 30/87 검증 수치 guard 반영.
- 2026-06-28: M2 8차 digest contract 경계와 focused 74 passed 검증 기준 반영.
- 2026-06-29: M2 9차 local digest quality harness와 `test_session_memory.py` 20 passed 검증 기준 반영.
- 2026-06-29: M2 11차 one-shot digest worker runner/default-off scheduler/CAS boundary와 focused 30 passed 및 주변 회귀 87 passed 검증 기준 반영.
- 2026-06-28: H1 2차 KPI metric status contract와 focused 10 passed 검증 기준 반영.
- 2026-06-28: H4 optional ko recognizer adapter 경계와 focused 47 passed 검증 기준 반영.
- 2026-06-26: 초판. `docs/ops/source-docs-gap-analysis-2026-06-26.md`와 SSOT 대시보드 "원천문서 갭 분석" 섹션을 요약·인덱스화.

View file

@ -15,8 +15,8 @@ Vignette 저장소의 모든 검증 수단(백엔드 단위 테스트, 웹 타
| 검증 | 작업 디렉터리 | 명령 | DB | API(8000) | 웹(5173) | 엔진GW(9099) | 브라우저 | 현재 통과 |
|---|---|---|---|---|---|---|---|---|
| 백엔드 단위 테스트 | `apps/api` | `python -m pytest app/ -q` | 불필요 | 불필요 | 불필요 | 불필요 | 불필요 | 178 pass |
| 엔진 게이트웨이 테스트 | `apps/api` | `python -m pytest engine_gateway/ -q` | 불필요 | 불필요 | 불필요 | 불필요 | 불필요 | 11 pass |
| 백엔드 단위 테스트 | `apps/api` | `python -m pytest app/ -q` | 불필요 | 불필요 | 불필요 | 불필요 | 불필요 | 기준선 178 pass |
| 엔진 게이트웨이 테스트 | `apps/api` | `python -m pytest engine_gateway/ -q` | 불필요 | 불필요 | 불필요 | 불필요 | 불필요 | 현재 27 pass |
| API 타입 생성 체크 | `apps/web` | `npm run check:api-types` | 불필요 | 불필요 | 불필요 | 불필요 | 불필요 | pass |
| 웹 타입체크 | `apps/web` | `npm run typecheck` | 불필요 | 불필요 | 불필요 | 불필요 | 불필요 | pass |
| 웹 빌드 | `apps/web` | `npm run build` | 불필요 | 불필요 | 불필요 | 불필요 | 불필요 | pass |
@ -42,15 +42,15 @@ Vignette 저장소의 모든 검증 수단(백엔드 단위 테스트, 웹 타
```sh
# apps/api
python -m pytest app/ -q # 앱 단위 테스트 (현재 178 pass)
python -m pytest engine_gateway/ -q # 게이트웨이 단위 테스트 (현재 11 pass)
python -m pytest app/ -q # 앱 단위 테스트 기준선 178 pass
python -m pytest engine_gateway/ -q # 게이트웨이 단위 테스트 현재 27 pass
```
수집만 빠르게 확인하려면:
```sh
python -m pytest app/ --collect-only -q # "178 tests collected"
python -m pytest engine_gateway/ --collect-only -q # → "11 tests collected"
python -m pytest app/ --collect-only -q # 기준선: "178 tests collected"
python -m pytest engine_gateway/ --collect-only -q # 현재: "27 tests collected"
```
> 참고: 실행 중 `PendingDeprecationWarning: Please use 'import python_multipart'`
@ -72,6 +72,8 @@ python -m pytest engine_gateway/ --collect-only -q # → "11 tests collected"
| `app/test_persona_review.py` | 페르소나 리뷰 워크플로 |
| `app/test_voice_service.py` | 음성 캐스케이드 서비스(STT/TTS) |
| `app/test_voice_ws.py` | 음성 WebSocket 경계 |
| `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 |
### 1.4 `engine_gateway/` 테스트
@ -262,18 +264,19 @@ VITE_API_BASE=http://127.0.0.1:8000 npm run e2e # 프록시 대신 API
- **`@single-run` 직렬 시나리오**: 17 tests (DB 영속화·세션 MVP·음성 성공경로 등)
- `e2e/voice-success.spec.ts`는 직접 `/voice/ws` 캐스케이드와 Session 마이크 UI를 함께 검증하며,
브라우저 `<audio>.play()`가 차단된 조건에서도 Web Audio buffer source 재생이 시작되는지 확인한다.
- 2026-06-27 최종 로컬 풀스택 검증: `PLAYWRIGHT_PORT=5174 npm run e2e` **113 passed**.
- 2026-06-27/28 기준선: `PLAYWRIGHT_PORT=5174 npm run e2e` **113 passed**.
레이아웃·시각 회귀 게이트(핵심 합격선):
| 게이트 | 스펙 | 구성 | 개수 |
|---|---|---|---|
| 세션 레이아웃 | `e2e/session-layout.spec.ts` | 4 테스트 × (desktop+mobile) | **8 / 8** |
| 시각 레이아웃 게이트 | `e2e/layout-visual-gate.spec.ts` | `@single-run`, 7개 화면 × 7개 폭 검사 | **7 / 7** |
| 레이아웃 포커스(재설계 화면) | `session-layout`·`session-review`·`admin`·`learner`·`settings`·`teacher`, `@single-run` 제외 | desktop+mobile 병렬 | **58** |
| 시각 레이아웃 게이트 | `e2e/layout-visual-gate.spec.ts` | `@single-run`, 9개 화면 × 7개 폭 검사 + 다크 테마 assertion | **9 / 9** |
| 레이아웃 포커스(재설계 화면) | `session-layout`·`session-review`·`admin`·`learner`·`settings`·`teacher`, `@single-run` 제외 | desktop+mobile 병렬 | **54** |
> `layout-visual-gate`는 7개 폭(390/720/861/900/1024/1280/1440)에서 가로 오버플로·잘린
> 컨트롤을 검사하고 전체 페이지 스크린샷을 `node_modules/.tmp/layout-gate/`에 남긴다.
> `layout-visual-gate`는 7개 폭(390/720/861/900/1024/1280/1440)에서 9개 핵심 화면의 가로
> 오버플로·잘린 컨트롤·다크 테마 적용을 검사하고 전체 페이지 스크린샷을
> `node_modules/.tmp/layout-gate/`에 남긴다.
> `session-layout`은 회기 전/활성 화면이 뷰포트를 벗어나지 않는지, 우측 패널이 코어 영역을
> 침범하지 않는지, 시작 후 실제 `session_id` URL에서 새로고침해도 활성 회기 상세가 유지되는지,
> 스트림 실패 시 미저장 전사가 남지 않는지를 검증한다.