Jev 기반 내담자 감정 상태와 응답 일관성 개선

This commit is contained in:
Yun Chan 2026-09-22 21:32:26 +09:00
parent 77f8421818
commit 8344bc2ad2
23 changed files with 3384 additions and 25 deletions

View file

@ -136,6 +136,7 @@
| [`decisions/backend-node-transition.md`](./decisions/backend-node-transition.md) | 백엔드 언어 방향(FastAPI 유지·Node 계약 우선 전환) |
| [`decisions/voice-s2s-poc.md`](./decisions/voice-s2s-poc.md) | 음성 s2s 2차 PoC 채택 판단 기준 |
| [`decisions/local-voice-stack.md`](./decisions/local-voice-stack.md) | 노트북 로컬 음성 스택 결정 — faster-whisper STT + MeloTTS TTS(둘 다 MIT), 설치 함정·cuDNN 제약, G7 게이트 provider 계약 |
| [`decisions/jev-client-affect.md`](./decisions/jev-client-affect.md) | Jev 감정 상태 판단 도입 결정 — 기존 내담자 생성 모델과 안전·단계 소유권을 유지하고, 한국어·다중 턴·지연 실증 뒤에만 승격 |
| [`decisions/outcome-alliance-measurement-ledger.md`](./decisions/outcome-alliance-measurement-ledger.md) | Outcome & Alliance append-only 측정 원장, source/perspective 경계, 전진 복구·롤백 결정 |
## 🧪 Phase 3 파일럿 (forward — 아직 미실행)

View file

@ -24,6 +24,7 @@
| G8-EXTERNAL | G8 실DB/public 사람 게이트를 실행한다. | 승인·보류·반려 사유와 append-only effect를 실제 DB·공개 경로에서 증명. local fixture는 대체 불가. |
| G7-EXTERNAL | 동의 기반 외부 음성 종료 게이트를 준비한다. | 장치 선택·명시 동의 후 3,120초 양방향 soak, 3,000초 공통 high-water, 독립 human voice-gain pack, canonical checker exit 0. |
| ANTHROPIC-001 | `claude_cli`와 Anthropic API live 동일성을 비교한다. | 기관 키를 게이트웨이 호스트에 승인 주입한 뒤 응답·계량·오류 표면화 비교. 키 주입 전에는 실행하지 않는다. |
| JEV-001 | 커밋 시 고정 artifact 갱신 절차를 실행하고, 충분한 한국어 독립 평가와 전체 지연 반복 비교를 마친다. | 구현은 수용했지만 품질 승격은 보류하며, 완료 조건은 위 게이트를 닫고 운영 배포 여부를 별도로 판단하는 것이다. 최신 probe·수용/반려 근거는 [Jev 결정문](./decisions/jev-client-affect.md)을 따른다. |
| VNET-001 | `vnet.18ka.net` 공개 전환의 외부 설정을 마친다. | DNS, Cloudflare zone 권한, Google redirect URI가 모두 준비된 뒤 live 검증. |
| PIPELINE-001 | Forgejo 기준 NAS 자동배포 hook을 구성한다. | 수동 git-container clone·SHA 검증·NAS build/compose 절차를 보존한 자동화와 rollback/approval 경계를 검증한다. |
| INGRESS-001 | Cloudflare tunnel 제거 지시의 대체 ingress gate를 닫는다. | NAS ingress 대체 경로의 보안·가용성·OAuth 경계를 검증한 뒤에만 tunnel retirement를 승인한다. |

View file

@ -0,0 +1,80 @@
# Jev 기반 가상 내담자 감정 상태
결정일: 2026-09-22. 상태: 감정 경로 구현과 OpenRouter 실제 판단 검증 수용. 운영 적용과 한국어 품질 승격은 미완료.
## 문제와 목표
기존 내담자는 페르소나·대화 기억·개방도·저항을 전달받지만, `SessionState.affect_state`를 턴마다 갱신하는 경로가 없다. 감정 저장 통로가 있는 것과 감정이 대화에 따라 변화하는 것은 다르다. 이번 변경은 감정의 지속성·혼합·변화를 실제 대사 생성에 연결한다. 사례 사실의 일관성, 감정의 개연성, 응답 지연은 별도로 평가한다.
## 근거와 적용 범위
- [TypeSafe 소개](https://docs.typesafe.ai/introduction): Jev는 자유 문장을 생성하지 않고 구조화된 선택·점수·확률을 반환한다. 질문별 독립 판단을 한 요청에 묶을 수 있다.
- [OpenRouter Decisions API](https://openrouter.ai/docs/api/api-reference/alphadecisions/submit-a-decisions-questions-and-answers-request): 사용자가 선택한 `POST https://openrouter.ai/api/alpha/decisions`, Bearer 인증, `state`·`model`·`questions`와 `answers`·`usage` 계약을 사용한다. 채팅 생성 endpoint를 사용하지 않는다.
- 사용자 지정 모델은 `~typesafe/jev-latest`다. 요청 식별자를 그대로 보내며 응답의 실제 모델을 별도로 기록한다. [모델 문서](https://docs.typesafe.ai/models)의 언어 제약에 따라 한국어 품질은 별도로 검증한다.
- [공식 발표](https://typesafe.ai/blog/introducing-system-one-models-and-jev)의 속도 수치는 회사 자체 평가다. Vignette 전체 응답 속도나 임상 정확도의 증거로 사용하지 않는다. 타입이 올바른 출력도 의미적으로 틀릴 수 있다.
- [Appraisal 기반 감정 에이전트 연구](https://journals.plos.org/plosone/article?id=10.1371/journal.pone.0301033)는 사건의 개인적 의미를 정서 표현에 연결하는 참고 근거다. 게임 에이전트 결과를 상담 타당성으로 확대하지 않는다.
- [EmoCharacter](https://aclanthology.org/2025.naacl-long.316/)는 역할 재현과 감정 충실도가 별도 평가 대상임을 보여 준다. 큰 모델이나 페르소나 프롬프트만으로 감정 품질이 보장되지 않는다.
## 확정 구조
마스킹된 페르소나·기억·현재 상담자 발화 → Jev의 병렬 감정 판단 → 코드가 소유하는 제한된 상태 전이 → 기존 대화 모델의 스트리밍 발화 순서다. 기존 위기 게이트는 Jev보다 먼저 적용한다. Jev가 위기 판단·라포·단계 전이·진단·교수자 평가를 대신하지 않는다.
9개 감정은 불안·슬픔·분노·수치심·죄책감·외로움·안도·희망·신뢰다. 각 감정의 강도를 독립적으로 유지하므로 안도와 죄책감, 희망과 불안이 동시에 높을 수 있다. 선택지 확률을 감정 강도로 오인하지 않고, 감정별 5단계 `Score`를 정규화한다.
영속 키는 `emotion_<dimension>`이며 기존 `affect_state` JSON과 회기 종료 snapshot을 사용한다. 기존 임상 키는 보존한다. 이전 값이 없으면 명시된 감정 기저선, 불안 기저선, 부정 정서, 무망감에 대응하는 희망값을 사용한다. 근거가 없는 다른 축의 초기값은 0이다. 이 초기화는 공학적 시작값이며 임상적으로 보정된 척도가 아니다.
높은 confidence의 전이는 `old + clamp(0.35 × (target − old), −0.15, +0.15)`다. 기본 문턱 0.65 미만은 아래 후속 개선의 분포 조건에 따라 작은 잠정 전이 또는 보류로 나눈다. 이 상수들은 검증 전 공학적 기본값이며 전문가 평가로 보정해야 한다. Jev가 판단한 값을 DB 상태에 직접 덮어쓰지 않는다.
감정은 대사·주저함·침묵·말투에 반영하되 수치와 내부 제어문을 발화하지 않도록 한다. 고정 사실과 사례 설정을 바꾸지 않으며, 내담자가 상담사 역할로 전환하지 않는다. 기존 시나리오 지시와 개방도 제약도 유지한다.
## 지연·장애·개인정보 계약
- 기존 `httpx` 연결을 재사용하고 9개 판단은 한 요청으로 묶는다. 기본 전체 deadline은 1.2초이며 자동 재시도는 하지 않는다.
- Jev 판단은 발화 전에 필요하므로 이 단계 자체는 지연을 추가한다. 전체 응답이 빨라졌다는 주장은 실제 첫 토큰·전체 응답시간의 기존 경로 대비 측정 없이는 하지 않는다.
- `VIGNETTE_CLIENT_AFFECT_PROVIDER=legacy|jev`로 명시적으로 선택한다. 기본은 기존 경로다. Jev 선택 시 키 누락·시간 초과·잘못된 응답을 다른 공급자로 숨겨 대체하지 않는다.
- `VIGNETTE_JEV_PROVIDER=openrouter|typesafe`의 기본은 `openrouter`이며 `OPENROUTER_API_KEY`를 사용한다. 직접 TypeSafe 연결은 명시적 선택과 별도 키·모델이 있어야 한다. 공급자와 모델을 자동 변경하지 않는다.
- OpenRouter가 confidence를 생략하면 `None`으로 보존하여 실제 0과 구분하고 해당 축을 보류한다. 선택적 legend와 확률분포는 제공될 때 검증하며, 원본 5수준 확률을 보존한다. 비용은 응답의 실제 값을 기록하고, 없으면 미상으로 남긴다.
- 실제 수련생 위기에는 외부 감정 판단을 호출하지 않는다. 실패하거나 취소된 턴의 새 감정은 영속화하지 않는다.
- 외부로 보내는 페르소나·기억·발화의 모든 텍스트를 개인정보·역할 이름 마스킹 경계에 통과시킨다. 계정·세션 식별자, 교수자 평가, API 키를 입력이나 로그에 넣지 않는다.
- 실제 모델·판단 지연·사용량·수용/보류 차원의 내부 provenance와 공개 학습자 응답을 분리한다.
## 검증과 승격 조건
계약 검증은 전이의 관성·복합 감정 유지·회기 이월·마스킹·위기 우선·취소·실패 시 미저장·generate/stream 일치를 확인한다. 테스트 대역을 사용한 결과는 실제 Jev 성능 증거가 아니다.
실측 러너의 합성 한국어 입력은 API 연결과 판단 결과를 수집하기 위한 자료다. 스스로 만든 정답으로 정확도를 선언하지 않는다. 2026-09-22 OpenRouter 실측은 8개 사례를 2회씩 호출하여 16/16 성공, 판단 지연 p50 252ms·p95 358ms, 실제 모델 `typesafe/jev-1.13-20260917`, 응답에 보고된 총비용 $0.001519896이었다. 144개 축 중 71개가 confidence 0.65 이상이었다. 로컬 증거는 `scratch/jev/openrouter-korean-live-verified.json`이다. 이 결과만으로 전체 대화 품질이나 속도가 검증되지는 않는다.
실측 초기에 2자리 반올림으로 확률합이 0.99인 정상 응답을 거부하는 문제가 발견됐다. 5수준의 반올림 최대 합산 오차(5 × 0.005)만 허용하도록 보정한 뒤 위 실측을 다시 수행했다. 각 확률의 범위와 차원 계약은 유지한다. 작업자 구현은 오케스트레이터가 diff를 읽고 어댑터 20 passed와 감정·마스킹 회귀 48 passed를 근거로 수용했다. 테스트 대역과 실제 호출 증거는 구분한다.
실제 대화 연결은 `scripts/probe-jev-dialogue.py --output scratch/jev/dialogue-live.json --turns 2`로 수집했다. 기존 경로와 Jev 경로 각각 2턴이 완료되고 합성 세션이 정리됐다. 기존 경로 첫 토큰 지연은 5972/2684ms, Jev 경로는 6873/3368ms였다. 기존 출력 검증 버퍼 때문에 첫 토큰 시각은 전체 완료와 거의 같다. 순서를 고정한 소수 사례이므로 성능 비교 결론이나 정확도 주장은 하지 않으며, 이번 관측에서 Jev가 전체 응답을 빠르게 만들지는 않았다.
전체 회귀는 API 1165 passed·1 failed·1 skipped, gateway 82 passed, API 타입 동기화 통과다. 실패 항목 `test_runtime_observation_artifact_matches_current_exact_case_execution`은 이번 미커밋 orchestrator 변경과 과거 고정 증거의 지문 불일치다. 기존 자료나 검증 조건을 완화하지 않았다. 현재 작업트리의 위기 기술 검증은 `scratch/jev/current-crisis-technical-observations.json`에 별도로 수집해 6/6 통과를 확인했다. 임상 외부 판정은 0건이며, 커밋과 공식 증거 갱신 시 고정 지문 게이트 재검증이 필요하다.
운영 승격에는 동일 조건의 기존 경로 대비 충분한 다중 턴 비교, 사례 사실 모순율, 역할 이탈, 감정 변화와 혼합의 전문가 검토, 첫 토큰 및 전체 응답의 p50/p95, 장애율이 필요하다. 로컬 연결 검증과 품질 승격을 구분한다. 로컬 비공개 설정은 Jev/OpenRouter로 활성화했으며 운영 배포는 수행하지 않았다.
## 후속 개선 설계 — 2026-09-22
[공식 confidence 설명](https://docs.typesafe.ai/confidence)에 따르면 confidence는 정답 확률이 아니라 결과 분포의 집중도를 요약한다. [Jev 1.13의 제한](https://docs.typesafe.ai/model-jaggedness/jev-1.13)은 점수를 실제 강도의 정밀 측정치로 보지 말라고 명시한다. 따라서 인접 강도 사이의 불확실성과 서로 먼 강도 사이의 불확실성을 구분한다.
후속 구현 계약은 다음과 같다. 높은 confidence의 기존 전이는 유지한다. confidence가 0.35 이상이고 높은 문턱보다 낮을 때, 유효한 분포를 정규화한 뒤 인접 두 수준의 확률 합이 0.80 이상인 경우에만 작은 잠정 전이(`alpha=0.15`, 최대 변화 0.075)를 허용한다. confidence 누락, 낮은 confidence에서 넓게 퍼진 분포나 양끝으로 나뉜 분포는 보류한다. 잠정 차원은 내부 메타데이터에 별도로 기록한다. 이 값들은 합성 연기의 공학적 정책이며 보정된 임상 기준이 아니다.
생성 입력은 기존 임상 상태를 보존하면서 새 감정의 긴 소수점 목록을 질적 강도와 짧은 연기 지시로 바꾼다. 최대 네 감정을 전달하되 반대 정서가 함께 존재하면 이를 남긴다. 고정 사실과 충돌하는 상담자의 회상 유도를 과거 기억으로 받아들이지 않도록 지시한다. Jev 질문은 감정 주체, 근거, 지시문 주입 방어와 고정 사실 우선순위를 보존해 압축한다.
비교는 같은 합성 발화와 고정 사실, 같은 생성 모델 설정으로 수행한다. 각 반복에서 기존/Jev 순서를 교대하고 첫 턴과 이후 턴을 분리한다. 사실 충돌 사례는 명시적인 기대 행동과 원문을 남기며 자동 정확도 점수로 포장하지 않는다. 전체 출력 검사, 위기 게이트, 모델·effort 선택, 오류 전파 계약은 유지한다.
### 후속 구현 판정과 실측
오케스트레이터는 어댑터·감정 전이·측정 러너의 diff를 직접 검토해 수용했다. 측정 러너의 첫 턴/이후 턴 구분 오류와 전이 테스트의 불일치 입력은 재작업 후 수용했다. 지시문 9개의 문자 합계는 2,942→2,186으로 25.7% 줄었다. 같은 8개 fixture를 2회씩 호출한 후속 판단은 16/16 성공, 동일 Jev 실제 버전, 입력 36,188→34,604토큰 및 비용 $0.001519896→$0.001453368로 4.377% 감소했다. 판단 지연은 p50 252→336ms, p95 358→567ms로 늘었다. 입력 절감과 지연 개선은 같은 의미가 아니다. 증거: `scratch/jev/improve-appraisal-after.json`.
후속 판단 144개 축에 기존 정책을 적용하면 50개가 보류된다. 새 정책에서는 같은 결과 중 42개가 잠정 전이 대상이 되고 8개는 보류된다(높은 confidence 94개). 이 비교는 전이 정책의 차이를 검증하며 감정 정확도 증거가 아니다.
대화는 각 버전에서 기존/Jev 각각 3턴×2회, 총 12턴씩 수집했다. 같은 fixture·측정 스크립트·gateway 지문과 생성 설정을 확인했다. model/effort는 기본 설정을 유지했으며 gateway의 모델 이름은 upstream 버전의 독립 검증으로 취급하지 않는다. 중간 실측에서 실제 발화에 없던 이름을 언급한 1건을 발견해 L4에 없는 말·이름·사건을 덧붙이지 않는 지시를 추가했다. 최종 12턴에서 그 오류는 재현되지 않았지만, 두 번의 합성 대화로 환각 방지를 보장하지 않는다. 잘못된 여동생 회상 유도는 변경 전후 모두 받아들이지 않았다.
| 전체 응답시간 | 변경 전 Jev | 최종 Jev | 최종 기존 경로 |
|---|---:|---:|---:|
| 첫 턴 p50 / p95 (각 2개) | 6,584.8 / 6,906.6ms | 5,978.4 / 6,463.1ms | 5,434.4 / 5,826.3ms |
| 이후 턴 p50 / p95 (각 4개) | 3,091.5 / 4,278.6ms | 2,537.8 / 2,860.9ms | 2,013.8 / 2,597.2ms |
첫 토큰은 전체 출력 검사 때문에 전체 완료 시각과 거의 같다. 전후 수치는 감소했지만 적은 표본·실행 시점·생성문 차이가 있어 코드 변경의 인과 효과나 일반적 속도 우위를 주장하지 않는다. 최종 동시기 비교에서도 Jev 경로는 기존 경로보다 느리다. 원문과 지문은 `scratch/jev/improve-before.json`, `improve-after.json`(중간 결함 포함), `improve-final.json`에 보존했다. 마지막 보고서 12/12 완료와 모든 합성 세션 정리를 확인했으며, 검증용 gateway만 종료했다.
후속 전체 API 회귀는 1,171 passed·1 failed·1 skipped다. 마지막 사실 지시 보강 후 감정/마스킹/페르소나 48개를 재검증해 통과했다. 측정 러너 3개와 현재 작업트리 위기 기술 사례 6/6도 통과했다. 남은 실패는 앞서 기록한 `matches_head` 미커밋 지문 게이트이며, frozen 자료·테스트 조건을 바꾸지 않았다. 증거는 `scratch/jev/improve-api.stdout.log`, `improve-crisis-technical-observations.json`이다. **판정: 구현과 계약 검증 수용, 일반적 속도 우위·한국어 감정 정확도·운영 승격 주장은 보류.**

View file

@ -303,6 +303,7 @@
<h1>지금 위치: <em>개선관리 원본 14개 재점검</em> — 2026-09-09 읽기 전용 확인에서 완료 13·검토 1(C-001)이다. REQ-009~011의 로컬 재검증은 수용했고, SHA `1306c524` 운영 배포 뒤 H09를 새 live 검증으로 PASS 확인했다. 2026-09-01·09-08 배포는 역사 증거이며, 9/9 배포가 기존 PASS 전체의 새 검증을 뜻하지는 않는다.</h1>
<p class="pulse-state"><b>실배포 CUA 128:</b> 카탈로그는 생성됐지만 전체 검증은 미완료다. H09(기록 검색·필터 복귀)는 SHA `1306c524`에서 PASS로 재확인했고 T18(감독·연구 drilldown)은 수정 배포 뒤에도 learner 브라우저만 있어 RED를 종결하지 않았다. 관리자와 일반 학습자 로그인에서 학습자의 `/admin`·`/teach`→`/learn` 차단, P4 추천·R09 잠금·R14 워크시트 보존을 확인했다. 순수 teacher·OFF 계정, 교수자 쓰기, 모바일과 독립 관찰자 공개 검증은 남아 있다.</p>
<p class="pulse-state"><b>2026-09-12 비넷 UX 감사:</b> <b>DONE · 로컬 시각 검수 수용</b>. 역할별 메뉴·모바일 라벨, 학습자 정보 위계와 긴 목록, 상담 시작 버튼 겹침, 교수·관리자 표의 넘침, 설정과 약관 배치를 수정했다. 전체 레이아웃 15 passed, 마지막 관리자 폭 보정 후 2 passed, 회기 8 passed, 접근성 44개(38+6) 통과와 직접 이미지 검수를 수용했다. 상세 판정·반려 기록·실제 API와 fixture 증거 경계는 <a href="./ops/ux-audit-2026-09-12.md">감사 기록</a>에 있다. 이후 SHA <code>e8b770e4d9023238bf210b412de75dcb98bfc08e</code>는 Forgejo master에 push했고, 17:55:31 KST 배포 시작 뒤 Pages 공개 배포와 NAS web 전용 교체를 18:06 KST까지 완료했다. API·engine·DB는 유지했다. 인증 내부 UI는 401 세션 만료로 미검증이므로 전체 운영 내부 화면 GREEN이나 전체 기능 출시 완료를 뜻하지 않으며 <a href="./ops/deployment-pipeline.md">배포 현황</a>을 따른다.</p>
<p class="pulse-state"><b>JEV-001 가상 내담자 감정 판단:</b> <b>로컬 구현 수용·품질 승격 검증 중</b>. 최신 probe는 결정문에 기록하며 성능 우위·정확도와 DONE 주장은 보류한다. 코드 기본값 <code>legacy</code>와 운영 미배포를 유지하고, 상세 수용/반려와 다음 게이트는 <a href="./decisions/jev-client-affect.md">Jev 결정문</a>을 따른다.</p>
<p class="pulse-state"><b>2026-08-31 배포 파이프라인 재정립(역사 기록):</b> git 관리·배포는 <code>git.chanpaca.net</code>(Forgejo) 중심으로 이관했고, <code>github.com</code>(origin)은 private 백업/미러로 유지한다. 배포 대상은 NAS Production(<code>docker-compose.nas.yml</code>, <code>vignette-prod</code>)이며 이 PC는 개발용이다. Cloudflare tunnel은 NAS ingress의 현재 경로이므로 대체 ingress 검증 전에는 제거하지 않는다. 문서 <a href="./ops/deployment-pipeline.md">deployment-pipeline.md</a>.</p>
<p class="pulse-state"><b>최근 확인된 배포 증거(2026-09-01 당시):</b> Forgejo master <code>dce85620</code>를 NAS에서 직접 clone·빌드해 <code>vignette-prod</code> api/engine/web을 교체했고, Cloudflare Pages production도 새 빌드(<code>index-BFKGXJQi.js</code>, 이전 세대 자산 보존)로 배포했다. 사전 dump <code>cab6826b…</code>(11.0MB·TOC 1783)·env 백업·구 이미지 보존으로 롤백 경로를 고정했다. 중복 활성 회기 14쌍 30행은 최신 유지 정책으로 종료(영수증 보존·삭제 0)하고 마이그레이션 20/21/22를 온라인 적용했다. 이 과정에서 mig22의 <code>name[]=text[]</code> 캐스트 결함과 master의 게이트웨이 openai provider 누락 회귀를 발견해 즉시 교정했다(테스트 71 passed). 배포 당시 public health <code>ok·db/engine true</code>, OpenAPI 200, <code>/auth/me</code> 401, dev-login 404, 데이터 집계 전후 동일(app_user 1380/sessions 618/turns 1743)을 확인했다. 증거: <a href="./ops/evidence/nas-prod-deploy-2026-09-01.json">nas-prod-deploy-2026-09-01.json</a>. <b>headful 실계정 폐루프도 당시 완료:</b> NAS env의 OAuth secret 오류(<code>invalid_client</code>)를 교정한 뒤 소유자 실계정 로그인→기존 회기 20건 표시(데이터 보존)→새 사례 회기 생성(mig22 실증)→실턴 SSE·AI 응답→종료→리뷰 생성까지 GREEN(sessions 619·turns 1745·case_profile 566). 일일 검증형 DB 백업 sidecar 첫 덤프도 11.2MB로 성공했다. <b>남은 운영·외부 게이트:</b> C-001 외부 임상 검수, NAS 재부팅 자동복구 smoke, 백업 실패 알림·off-host 복제.</p>
<details class="pulse-details">
@ -1036,6 +1037,7 @@
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>과거 Live2D demo 자산 제거</b><p>Mao/Haru 샘플, Pixi/Cubism 런타임, 공개 <code>/live2d/*</code> 캐시 잔여 접근을 차단했다.</p></div><div><b>산출물</b><p><code>apps/web/functions/live2d/[[path]].js</code>, SVG 도형 기반 파라미터 리그</p></div><div><b>검증</b><p>운영 <code>/live2d/mao/*</code>, Cubism core <code>404</code></p></div></div>
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>프로세스 난립 정리</b><p>운영 확인용 프로세스와 로컬/Tailnet 검증용 dev 프로세스를 의도적으로 분리해 유지한다.</p></div><div><b>산출물</b><p>9099 engine, 8001 prod API, 8000/8010 dev API, 5173 web, cloudflared tunnel 1개</p></div><div><b>검증</b><p><code>Get-NetTCPConnection</code>에서 대상 포트별 listener 확인</p></div></div>
<div class="task-row"><div><span class="task-status s-plan">GATE</span></div><div><b>한신대 공문/데이터 거버넌스 게이트 정리</b><p>SSO 클레임, 추가 축어록 수급, 미성년 원본 활용동의, 개인정보 처리방침을 P1 진입 전 외부 의존성으로 명확히 둔다. 로컬 문서 골격은 준비됐지만 한신대/데이터 steward의 written evidence는 아직 필요하다.</p></div><div><b>산출물</b><p><code>docs/ops/hanshin-data-governance-gate.md</code>, 공문 질의 항목, 동의 범위 체크리스트, SSO claim mapping 표</p></div><div><b>검증</b><p>문서 artifact 작성 완료; IRB 게이트가 아니라 데이터/SSO 게이트로 외부 증거 필요 상태 유지</p></div></div>
<div class="task-row"><div><span class="task-status s-doing">JEV-001</span></div><div><b>Jev 감정 판단 실증과 기존 내담자 경로 반복 비교</b><p>수용: 구현, app 1171 passed/1 failed/1 skipped, engine 82 passed, API types 통과와 위기 기술 관찰 6/6 pass·임상 결정 0. 반려: 성능 우위·정확도·품질 승격 및 full-suite GREEN 주장.</p></div><div><b>산출물</b><p>runner/probe와 artifact exact-match evidence는 <a href="./decisions/jev-client-affect.md">Jev 결정문</a>에서 관리한다.</p></div><div><b>검증</b><p>frozen artifact는 HEAD와 일치하지만 현재 미커밋 작업트리와 불일치한 exact-match test를 보존한다. 다음은 커밋 시 artifact 갱신, 한국어 독립 평가, 전체 지연 반복 비교, 운영 미배포다.</p></div></div>
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>한신대 실사용 피드백 1차 개선팩</b><p>오류분석 자료를 컨텍스트 보존, 내담자 출력 품질, 리뷰 privacy/오류문구 UX, 교수자 설명 가능성으로 분해했고 1차 구현을 완료했다. P1은 role mapping + client-only history injection을 원자 패치로 닫았고, P2/P3/P4는 저장형 fallback 없이 빠른 UX 개선으로 배포 가능한 상태다.</p></div><div><b>산출물</b><p><code>gateway._split_messages(ai_role=client)</code>, <code>guardrail.sanitize_client_reply()</code> 품질 게이트, SSE full-response buffer, 리뷰 표시 자연어화, raw error mapper, 교수자 <code>AI 평가 범위</code> 패널, 계획/레드팀/대레드팀 문서</p></div><div><b>검증</b><p><code>engine_gateway/test_gateway_model.py app/test_orchestrator_masking.py app/test_client_reply_quality.py app/test_session_turn_persistence.py</code> 84 passed, <code>npm run typecheck</code>, <code>npm run check:api-types</code>, <code>session-review.spec.ts</code> focused 8 passed, <code>layout-visual-gate.spec.ts</code> 12 passed, <code>session-layout.spec.ts</code> 8 passed.</p></div></div>
</div>
<div class="source-note"><b>팀장 판정:</b> 로컬 회귀와 공개 prod-safe 게이트는 통과했다. 2026-06-29 수동 Cloudflare Pages production 배포가 현재 custom domain asset으로 확인됐고, 운영 반영 전 남은 핵심 증거는 공개 Google OAuth 실제 <code>/turn</code> smoke다. 런타임 mock/demo 자산은 운영 번들에서 제거하고 테스트 fixture만 남긴다.</div>

View file

@ -124,11 +124,11 @@ DB readiness는 auth/admin 테이블뿐 아니라 세션 read-model 핵심 테
- `persona.build_turn_messages(...)`로 L0~L6 `EngineMessage[]` 조립.
- 회상/핀/직전 턴 등 *주입 텍스트도 전부 다시 마스킹*한다(`_mask_optional_text` 등).
- **`run_turn_generate(ctx, engine, *, eval_hook=None, log_hook=None)`** (4~8, 동기/폴백/테스트 경로):
`engine.generate()`로 응답 한 번에 수신 → `guardrail.sanitize_client_reply()` → 수단정보 누출 시
실제 위기 게이트를 통과한 뒤 `VIGNETTE_CLIENT_AFFECT_PROVIDER=jev`일 때만 재마스킹된 페르소나·기억·현재 상담자 발화와 결정론 상태를 Jev에 한 번 보낸다. 수용된 감정 전이로 L3를 다시 조립한 다음, 기존 `engine.generate()`로 응답 한 번에 수신 → `guardrail.sanitize_client_reply()` → 수단정보 누출 시
안전 대체 응답("…(말을 잇지 못하고 잠시 침묵한다)")으로 치환 → eval/log 훅 순차 적용 → `TurnResult` 반환.
훅 예외는 모두 비치명적으로 흡수(상담 루프를 막지 않음).
기본 Jev transport는 OpenRouter Alpha Decisions(`https://openrouter.ai/api/alpha/decisions`)이며 `VIGNETTE_JEV_PROVIDER=typesafe`일 때만 직접 TypeSafe를 쓴다. 두 provider는 키 누락·시간 초과·잘못된 응답에서 서로 fallback하지 않고 생성 오류로 표면화한다. confidence 누락은 `None`으로 해당 차원을 보류한다. `VIGNETTE_JEV_MIN_CONFIDENCE` 설정값(기본 `0.65`) 이상은 기존 전이를 적용하고, 그 high threshold 미만이면서 `>=0.35` 및 원 probabilities를 정규화한 인접 두 구간 합 `>=0.80`일 때만 alpha `0.15`·변화 cap `0.075`의 잠정 전이를 적용하며 메타데이터에 tentative를 남긴다. 원 probabilities는 보존하고, 판단 질문은 압축하며, 질적 감정은 최대 4개로 제한하고 고정 사실을 바꾸지 않도록 지시한다. provider가 actual 비용을 반환하면 별도 감정 판단 provenance에 그대로 기록한다. Jev는 위기·저항·단계·라포·교수자 평가의 소유자가 아니다.
- **`run_turn_stream(ctx, engine, *, log_hook=None)`** (4~8, 기본 UX 경로):
게이트웨이 SSE 원시 라인을 받아 `token | done | safety | error`로 재방출.
위와 같은 Jev 감정 판단을 생성 요청 직전에 적용한 뒤 게이트웨이 SSE 원시 라인을 받아 `token | done | safety | error`로 재방출한다.
출력 가드레일은 *누적 텍스트* 기준으로 수단정보를 스캔하고, 발견 시 `safety` 이벤트 + 안전 대체로 종결한다.
세션 라우트는 client 응답과 결정론 상태를 먼저 영속화하고 `done`을 방출한다. fast-loop evaluator는
백그라운드 태스크로 실행해 같은 learner turn의 normalized 평가 row를 교체 저장하며, 평가 기반 코칭 충전도
@ -152,9 +152,9 @@ DB readiness는 auth/admin 테이블뿐 아니라 세션 read-model 핵심 테
| L6 | 직전 K턴 맥락(히스토리) + 이번 발화(L5) | ❌ | 상담자=user, 내담자(자기)=assistant 매핑 |
메시지 순서: `system(L0+L1, cache)` → `system(L2, cache)` → `system(L3)` → `system(L4)` →
assistant/user 히스토리(L6) → `user(이번 마스킹 발화, L5)`. 현재 Python gateway의
`_split_messages()` 경계는 system 묶음과 마지막 user payload만 소비한다. L6의 system 외
history를 실제 프롬프트에 직렬화하는 변경은 별도 프롬프트 동작 패치로 다룬다.
assistant/user 히스토리(L6) → `user(이번 마스킹 발화, L5)`. Python gateway의
`_split_messages()`는 역할을 보존한 L6 history를 현재 user payload에 직렬화한다. 상주 내담자 세션을
재사용하는 후속 턴은 resident 대화기록과 같은 history를 중복 주입하지 않도록 `current_user_payload`만 쓴다.
**안전 불변식**(`L0_SAFETY`, docstring R4/R5/M6):

View file

@ -192,6 +192,13 @@ npm install
| `ENGINE_MODE` | `claude_cli` | `claude_cli` / `claude_api` / `codex_cli` / `agy_cli` 공급자 라우팅. 실제 운영 변경은 관리자 드롭다운이 DB에 저장 |
| `ENGINE_GATEWAY_SHARED_SECRET` | 빈 값 | 선택 인증. NAS/원격 preview에서는 API와 gateway에 동일한 32자 이상 비-placeholder 값을 설정. 빈 값은 기존 로컬 9099 호환 |
| `VIGNETTE_LIVE_CLIENT_PROVIDER` | `claude_cli` | 실시간 내담자 AI 전용 lane. 관리자에서 선택한 evaluator/review 공급자와 분리해 회기별 Claude 상주 세션을 재사용 |
| `VIGNETTE_CLIENT_AFFECT_PROVIDER` | `legacy` | 내담자 감정 판단 경로. 기본 `legacy`는 기존 운영 동작을 유지하며, `jev`는 아래 Jev transport 설정을 사용한다 |
| `VIGNETTE_JEV_PROVIDER` | `openrouter` | Jev transport. 기본 `openrouter`는 Alpha Decisions endpoint를 쓰며, 직접 TypeSafe는 `typesafe`를 명시했을 때만 지원한다. 두 경로는 서로 fallback하지 않는다 |
| `OPENROUTER_API_KEY` | 설정된 비밀값 | 기본 OpenRouter Jev credential. 원문을 명령·로그·문서에 넣지 않는다 |
| `TYPESAFE_API_KEY` | 설정된 비밀값 | `VIGNETTE_JEV_PROVIDER=typesafe`일 때만 쓰는 직접 TypeSafe credential. 원문을 명령·로그·문서에 넣지 않는다 |
| `VIGNETTE_JEV_MODEL` | `~typesafe/jev-latest` | 기본 OpenRouter Jev 판단 모델 |
| `VIGNETTE_JEV_TIMEOUT_SECONDS` | `1.2` | Jev 판단 전체 deadline(초). 재시도하지 않는다 |
| `VIGNETTE_JEV_MIN_CONFIDENCE` | `0.65` | 설정값(기본 `0.65`) 이상은 기존 감정 전이, confidence 누락은 `None` hold. 그 high threshold 미만에서만 `>=0.35`와 정규화 인접 2구간 mass `>=0.80`이면 tentative 전이(alpha `0.15`, cap `0.075`) |
| `AUTH_DEV_LOGIN_ENABLED` | `true` | dev-login 엔드포인트 활성화 |
| `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"]` | 학습자·교수자·관리자 공간 접근과 승인 상태를 부여할 슈퍼 관리자 이메일 |
@ -220,6 +227,24 @@ npm install
> 참고: 프로세스 환경변수(`$env:KEY`)는 `.env`보다 우선한다. 일회성 오버라이드에 쓸 수 있다.
Jev는 기존 내담자 생성 모델을 바꾸지 않는다. `VIGNETTE_CLIENT_AFFECT_PROVIDER=jev`일 때만 직전 감정 판단을 추가한 뒤 기존 생성 경로로 넘긴다. 기본 transport는 `VIGNETTE_JEV_PROVIDER=openrouter`, 모델은 `VIGNETTE_JEV_MODEL=~typesafe/jev-latest`, endpoint는 `https://openrouter.ai/api/alpha/decisions`다. [OpenRouter Alpha Decisions 공식 API](https://openrouter.ai/docs/api/api-reference/alphadecisions/submit-a-decisions-questions-and-answers-request)의 요청 계약을 따른다. `typesafe`는 직접 TypeSafe를 명시한 경우에만 사용하며 어느 경로도 다른 쪽으로 fallback하지 않는다.
이미 해당 provider의 키가 설정된 로컬 환경에서는 아래 runner로 실제 판단 메타데이터를 수집할 수 있다.
```powershell
py -3.11 -X utf8 scripts/evaluate-jev-client.py --output outputs/jev-client-evaluation.json
```
원 probabilities는 보존하고 판단 질문은 압축하며, 질적 감정은 최대 4개로 제한해 고정 사실을 바꾸지 않도록 지시한다. 최종 반복 probe는 구현을 수용했지만 이번 비교에서 기존 경로가 더 빨랐고, 작은 표본·시점·응답 차이 때문에 일반 성능 우위와 품질 승격은 보류한다. 이 로컬 활성화는 코드 기본값 `legacy`나 운영 배포 상태를 바꾸지 않으며, 상세 결과·한계·다음 게이트는 [Jev 결정문](../decisions/jev-client-affect.md)을 따른다.
반복 비교에는 `probe-jev-dialogue.py`를 쓴다.
```powershell
py -3.11 -X utf8 scripts/probe-jev-dialogue.py --output outputs/jev-dialogue-probe.json --repeats 2 --phases legacy,jev --label local-comparison
```
`--repeats`, `--phases`, `--label`은 각각 phase 묶음 반복, 비교 대상, 보고서 식별자다. 첫 turn과 이후 turn을 분리해 요약하고, 짝수 반복은 phase 순서를 뒤집어 고정 순서 편향을 줄인다. 이는 provider cache 상태 측정이나 품질·성능 우위 판정이 아니다.
### 2.2 실행
```powershell

View file

@ -36,6 +36,7 @@ npx playwright test e2e/uc-session-conversation.spec.ts --grep '스트림 도중
| G8-EXTERNAL | 실DB/public 사람 게이트 | 승인·보류·반려와 append-only effect의 실제 public proof | local fixture와 route E2E는 대체 불가 |
| G7-EXTERNAL | 동의 기반 음성 외부 proof | 3,120초 soak, 3,000초 overlap high-water, 독립 human pack, checker exit 0 | 장치 선택·명시 동의 전 마이크를 열지 않음 |
| ANTHROPIC-001 | provider live 동일성 비교 | 승인 주입한 기관 키로 응답·계량·오류 표면화 비교 | credential 취급·주입은 소유자 경계 |
| JEV-001 | 한국어 독립 평가와 전체 지연 반복 비교 | 감정·사실 품질과 지연을 평가하고, 커밋 시 고정 evidence 지문을 재검증한다. | 구현 수용·품질 승격 보류, 운영 미배포. 상세 수용/반려·최종 probe는 [Jev 결정문](../decisions/jev-client-affect.md)을 따른다. |
| VNET-001 | vnet 공개 전환 | DNS·Cloudflare zone 권한·Google redirect URI와 live 검증 | 현재 NAS ingress를 넓히지 않음 |
| PIPELINE-001 | Forgejo 기준 NAS 자동배포 hook | 수동 git-container clone·SHA 검증·NAS build/compose 절차를 보존한 자동화와 rollback/approval 경계 | 현 수동 실증 절차를 우회하지 않음 |
| INGRESS-001 | Cloudflare tunnel 제거 지시의 대체 ingress gate | NAS ingress 대체 경로의 보안·가용성·OAuth 경계를 검증한 뒤 tunnel retirement 승인 | tunnel 제거는 사용자 지시이나 대체 ingress 검증 전 미이행 |