대시보드 폴드아웃/드릴다운 정리 + 페르소나 역린·misconduct 반응 + 게이트웨이 격리·RAG 비차단 수정
SSOT 대시보드:
- 한신대 기술분석 PDF(19쪽) 정합성 분석 + 이번 세션 발견 섹션 추가
- 섹션 폴드아웃(접기)·상단 목차(드릴다운)·모두 펼치기/접기 — 내용 보존, 레이아웃만 정리
페르소나 반응 강화('저항·반응 조절' 핵심 차별):
- PersonaCard.triggers(역린) 필드 + CCD 핵심상처 파생 역린 블록
- L0에 무례·모욕·조롱 시 현실적 동맹 균열 반응 지침
버그·성능 수정(라이브/E2E로 포착):
- 게이트웨이 페르소나 격리: --append-system-prompt를 --system-prompt(교체)로 + --exclude-dynamic-system-prompt-sections (내담자 캐릭터 붕괴·개발맥락 누출 차단)
- RAG: 임베더 동기 로드(약 7-13초)를 _warm_rag_caches 백그라운드 warm으로(세션 생성 블로킹 회귀 수정)
- voice TTS RMS 데드힌트 제거, init_state OpennessParams 파라미터객체화
- 한국어 PII(날짜·금액·주소) 마스킹 보강
- 레이아웃 시각 게이트: 폼 컨트롤 값 스크롤 오탐 제외(7/7)
검증: 백엔드 84/84, E2E 42(데스크톱 27·모바일 11·아바타 4), 시각 게이트 7/7
This commit is contained in:
parent
cb2aebd76c
commit
085460b5e0
327 changed files with 31226 additions and 1829 deletions
63
docs/ops/backlog-2026-06-26.md
Normal file
63
docs/ops/backlog-2026-06-26.md
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
# Vignette 백로그 — 2026-06-26
|
||||
|
||||
이 세션(레이아웃 시각 수용 마감 + live 운영 증거 라운드) 이후 남은 항목을 한곳에 모은다.
|
||||
대시보드(`docs/dev_dashboard.html`)의 상태와 일치한다. 운영 원칙상 가짜 증거로 DONE 표기하지 않으므로,
|
||||
실증/외부 의존/소유자 결정이 필요한 항목은 여기서 추적한다.
|
||||
|
||||
분류: **B1 비차단 폴리시** · **B2 환경 제약(증거 생산 불가)** · **B3 소유자 결정** · **B4 외부 거버넌스**
|
||||
|
||||
> **B0. 원천문서 갭 분석 (2026-06-26 추가)** — 한신대 산학협력 원천문서 5종 정독으로 도출한 "부족한 부분"(critical 3 / high 4 / medium+ 6)은 **SSOT 대시보드** `docs/dev_dashboard.html` "원천문서 갭 분석" 섹션과 상세 `docs/ops/source-docs-gap-analysis-2026-06-26.md`에서 추적한다. C1 사례개념화 산출물·C2 위기개입 프로토콜·C3 이론모드/CBT가 신규 critical. 이는 아래 B1~B4보다 상위 우선순위 입력이며, 콘텐츠 정의는 임상팀(구훈정·어유경) 소유라 코드는 '편집 가능 구조'만 선제 구축한다.
|
||||
|
||||
---
|
||||
|
||||
## B1. 비차단 코스메틱 폴리시
|
||||
|
||||
- [x] **공용 셸 사이드바 구분선 전체 높이** — (2026-06-26 처리) `.vg-nav border-right`(sticky·뷰포트 높이)를 제거하고, `.vg-shell__body` 컨테이너 배경 하어라인(`linear-gradient` 1px×100%, `--nav-cur`로 그리드·구분선 폭 동기화)으로 본문 그리드 전체 높이를 따라가는 세로 구분선을 그린다. 1024 축소폭은 `--nav-cur: var(--nav-w-collapsed)`로, 720 이하(상단 가로바 전환)는 `background-image:none`으로 정합. `components/shell/shell.css`. **검증: `npm run typecheck` PASS + `vite build` PASS.** 시각 회귀 게이트(gate 7/7 + 포커스 E2E + session-layout 8/8)는 web+api+DB 스택(Docker 미기동·secrets 부재)으로 이 환경 미실행 — 스택 가용 시 1회 시각 확인 권장.
|
||||
- [ ] **빈상태 컬럼 높이 여백** — session-review 1280 masonry 우측 컬럼, session-prestart 우측 '진행 초점' 컬럼, learner-home 1440 하단 등 데이터 0건 빈상태에서만 두드러지는 컬럼 높이 불균형/여백. 실데이터면 채워진다. **(미착수, 의도적)** 0건 전용 시각 균형이라 러닝 앱에서의 시각 반복 없이 블라인드 수정 시 악화 위험 → 스택 가용 시 시각 확인하며 처리. 빈상태 전용 균형 레이아웃 채택 자체가 별도 폴리시 결정.
|
||||
- [x] **learner-home 로딩 스켈레톤 밀도(390)** — (2026-06-26 처리) '연습 대상' 로딩 스켈레톤을 빈 div 단일 셰이머에서 실제 카드 구조(마크 박스 + 이름/메타/요약 2줄 플레이스홀더)를 모사하도록 밀도 보강. `LearnerHome.tsx` (`.lh-skel__box`/`.lh-skel__line*`), reduced-motion 분기 동반 갱신. **검증: `npm run typecheck` PASS + `vite build` PASS.**
|
||||
|
||||
검증 기준: 변경 후 `npm run typecheck`, `e2e/layout-visual-gate.spec.ts`(7/7), 레이아웃 포커스 E2E(54), `e2e/session-layout.spec.ts`(8/8) 무회귀. (E2E 게이트는 web+api+DB 스택 필요 — 현재 워크스테이션은 Docker 미기동·API secrets 부재로 미실행. 코드 변경은 typecheck+vite build로 1차 검증함.)
|
||||
|
||||
---
|
||||
|
||||
## B2. 환경 제약 — 이 워크스테이션에서 증거 생산 불가
|
||||
|
||||
- [ ] **공개 Google OAuth 실제 `/turn` proof** — 로그인 가능한 계정으로 `storageState` 캡처 후 `E2E_PUBLIC_AUTH=1` + `chromium-public-auth` 1회 통과 필요. (소유자 지시로 이번엔 보류.)
|
||||
- [ ] **음성 캐스케이드 live** — 실제 Deepgram STT / OpenAI TTS provider 키, 물리 마이크, 공개 WSS 50분 양방향 실측. provider 키/하드웨어 부재로 미실행(백엔드 계약·UI synthetic 경로는 단위/E2E로 증명됨).
|
||||
- [ ] **claude_cli ↔ Messages API 폴백 동일성** — `ANTHROPIC_API_KEY`가 있어야 Messages API 경로를 돌려 동일성 검증 가능. claude_cli 경로는 게이트웨이 probe로 live 실측 완료.
|
||||
- [ ] **저항엔진 openness 곡선 DB 실증** — `app.turn_client_state`가 dev DB에 0행. 공감 vs 조언점프 곡선을 DB로 보이려면 다수 실턴(real LLM) 실행 + per-turn client_state 적재 경로 가동 필요. 현재는 `app.test_state_machine_resistance` 단위테스트로 결정론적 증명.
|
||||
- [ ] **마스킹 게이트 운영 외부-send 관측** — `audit.llm_call_log` 0행(앱이 payload를 적재하지 않음). 엔진으로 나가는 실제 요청 payload를 캡처해 raw PII 부재를 관측하려면 로깅 경로 또는 프록시 캡처 필요. 현재는 `app.test_orchestrator_masking`로 generate/stream payload에 phone/email/RRN 부재를 증명.
|
||||
- [ ] **재부팅 후 watchdog smoke** — `watch-public-runtime.ps1` + Scheduled Task가 재부팅 후 엔진/API/터널을 복구하고 public `/turn`이 통과하는지 실측. 재부팅 불가로 미실행(parser/ check-only 경로는 확인됨).
|
||||
- [ ] **상주 엔진풀 RSS 실측** — probe로 TTFT/세션재사용/cost는 live 확인. 프로세스 RSS(메모리)는 별도 OS 계측 항목으로 남김.
|
||||
|
||||
---
|
||||
|
||||
## B3. 소유자(윤찬) 결정 필요 — 단독 결정 시 월권
|
||||
|
||||
> 결정되면 대시보드 `결정 필요` 탭과 관련 Phase 항목을 갱신한다.
|
||||
|
||||
- [ ] **s2s 2차 PoC 채택 여부** — 캐스케이드만 / s2s 2차 PoC 유지. 영향: 음성 아키텍처·전송 비용·로그 보존. 기준표 `docs/decisions/voice-s2s-poc.md`.
|
||||
- [ ] **fast-loop 외부 API 허용** — 로컬 전용 / 외부 Haiku·Solar 폴백 허용. 영향: 데이터주권·레이턴시·장애 대응.
|
||||
- [ ] **재귀학습 fine-tuning 범위** — few-shot 자동갱신만 / fine-tuning 포함. 영향: 동의서·데이터셋 품질·운영 리스크.
|
||||
- [ ] **자유연습 기본값** — 기본 ON(+피드백 권장 배지+교수자 토글) / 교수자 승인 후 ON. 영향: 학습자 자율성·평가 품질·안전 정책.
|
||||
- [ ] **추가 축어록 수급 규모·일정** — 목표 N건/일정 확정. 영향: 평가 타당도·페르소나 다양성·Phase 3 측정력.
|
||||
|
||||
---
|
||||
|
||||
## B4. 외부 거버넌스 — 한신대/데이터 steward 서면 증거
|
||||
|
||||
- [ ] **한신대 데이터/SSO 거버넌스 게이트** — SSO claim mapping, 추가 축어록 수급, 미성년 원본 활용동의, 개인정보 처리방침. 로컬 문서 골격 `docs/ops/hanshin-data-governance-gate.md` 준비됨, 외부 서면 증거 필요. (소유자 지시로 이번엔 보류.)
|
||||
|
||||
---
|
||||
|
||||
## Phase 3 파일럿 게이트 (실참여자 필요)
|
||||
|
||||
- [ ] 20명 교육용 파일럿 운영 / 효과성·KPI 측정(SUS·자기효능감·κ/ICC·환각률) / 재귀학습 데이터셋 1차 산출 / 개인정보·동의 감사. 문서/checker는 준비됨(`docs/phase3/*`, `scripts/check-phase3-artifacts.py`), 실제 evidence는 파일럿 시점.
|
||||
|
||||
---
|
||||
|
||||
## 이번 세션에 닫은 것(참고)
|
||||
|
||||
- 레이아웃 시각 수용: 게이트 7/7 + 적대적 재검수 7/7 accept + 코스메틱 폴리시(공용 셸 1건 제외) — DONE.
|
||||
- live 운영 증거: engine config 운영값, 상주 엔진풀 probe(TTFT/cost/세션재사용), Postgres RLS/audit smoke 5 checks PASS, turn cost telemetry(app.turns 13행) — DONE.
|
||||
- 부수: RLS smoke 스크립트 cleanup RLS 컨텍스트 버그 수정, `vignette_app`(NOBYPASSRLS) dev 역할 프로비저닝.
|
||||
161
docs/ops/code-quality-research-2026-06-26.md
Normal file
161
docs/ops/code-quality-research-2026-06-26.md
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
# Vignette 코드품질 연구 보고서 (2026-06-26)
|
||||
|
||||
> SSOT: `docs/dev_dashboard.html`. 이 문서는 코드품질 리팩토링 연구의 상세 근거다.
|
||||
> 방법: 5개 차원 병렬 분석(데드코드·과다파라미터·중복·SSOT·추상화) + 방법론 연구 → 종합(워크플로우 code-quality-research).
|
||||
> 주의: 분석의 'location/참조 0건'은 착수 전 코드 재확인 권고. 자기수정 포함(예: make_eval_hook은 sessions 경로 주입 완료, voice 경로만 누락).
|
||||
|
||||
---
|
||||
|
||||
# Vignette 코드품질 개선 연구 보고서
|
||||
|
||||
## 1. 총평 — 임팩트 큰 리팩터 Top 3
|
||||
|
||||
차원별 분석 5종(데드코드, 파라미터 객체화, 중복 로직, SSOT, 추상화/패턴)과 모노레포 실측 구조를 종합할 때, 투자 대비 효과가 가장 큰 작업은 다음 셋이다.
|
||||
|
||||
1. **REST/WebSocket 턴 처리 파이프라인 중복 제거 (구조적 최대 부채).** `routes/sessions.py`의 `submit_turn`/`stream_turn`과 `routes/voice.py`의 `_run_turn_and_speak`가 동일한 도메인 절차(세션 로드→오너십 검증→`prepare_turn`→`run_turn_generate`→학습자/내담자 `TurnRecord` append→state update)를 Type-2/Type-3로 복제하고 있다. 단순 중복을 넘어 **실데이터 불일치**가 이미 존재한다: stage 표기가 채널마다 다르고(`sessions.py`는 한글 `_stage_label`, `voice.py`는 enum `.value` 원시값), 음성 경로는 `eval_hook` 미주입으로 평가가 누락되며, `voice.py`는 빈 `RecallContext()`를 매 턴 새로 만들어 회상 주입이 빠진다. 공통 `record_completed_turn`/`load_owned_session`/`SessionRepository`로 추출하면 중복·드리프트·기능 누락을 한 번에 해소한다.
|
||||
|
||||
2. **백엔드 Pydantic ↔ 프론트 `lib/api.ts` 계약 SSOT화 (드리프트 실증됨).** `routes/sessions.py`의 ~14개 응답 모델이 `api.ts`에 수기 미러되어 있고(주석에 "계약 미러" 명시), 이미 드리프트가 발생했다 — 백엔드 `TurnResponse.crisis_kind`, `SessionEndResponse.end_state`가 TS 타입에 없다. OpenAPI → openapi-typescript 단방향 자동 생성으로 전환하면 후속 모든 프론트 리팩토링의 토대가 되고, 타입만 다루므로 런타임 리스크가 낮다.
|
||||
|
||||
3. **죽은 확장점/계약 정리 (LogHook·eval_hook·tier 라우팅·Live2D 자산).** 잘 설계됐으나 미배선된 seam이 다수다: `LogHook`/`log_hook`은 caller 0건의 완전한 데드코드, `make_eval_hook`은 정의·export·테스트만 있고 호출 0건, 게이트웨이 `GenerateRequest.tier`(client=Sonnet/evaluator=Opus/fast=Haiku)는 항상 `DEFAULT_MODEL`로 무시되는 dead-contract, Live2D 모델 자산 트리는 fetch 주체가 없는 고아 자산이다. 각각 "배선" 또는 "삭제"로 결론지어 API 표면을 줄이면 이후 작업 비용이 줄어든다.
|
||||
|
||||
> 참고: 분석 JSON은 일부 항목에서 자기수정을 포함한다. 예컨대 "make_eval_hook 완전 미주입"은 부정확하며 정확한 결함은 **음성 경로 한정 `eval_hook` 누락**임을 명시하고 있다(`sessions.py:916`은 주입, `voice.py:307`은 미주입). 본 보고서는 이 수정된 판정을 따른다.
|
||||
|
||||
---
|
||||
|
||||
## 2. 방법론 요약 (차원별 접근·도구·프로세스·안전장치)
|
||||
|
||||
전제: 현재 레포에 `pyproject.toml`/`ruff`/`mypy`/`eslint`/`knip` 설정이 **없고** `requirements.txt`만 존재한다. 따라서 0순위는 **측정 도구를 레포에 고정(pin)하고 베이스라인을 스냅샷하는 것**이다.
|
||||
|
||||
**공통 안전 원칙:** ① 1 PR = 1 관심사, ② 순수 리팩토링 커밋과 동작 변경 커밋을 절대 섞지 않음, ③ 리팩토링 전 해당 모듈 테스트가 green인지 확인(없으면 characterization test 선작성), ④ `git mv`/IDE rename으로 diff 노이즈 최소화.
|
||||
|
||||
| 차원 | 권장 접근 | 핵심 도구 | 안전장치 |
|
||||
|---|---|---|---|
|
||||
| **0. 베이스라인** | `apps/api/pyproject.toml`·`apps/web/knip.json` 신설, 도구 버전 pin, `scripts/quality-baseline.ps1`로 수치 덤프 | ruff·vulture·mypy·coverage·knip | CI 게이트는 `--exit-zero`(경고만)로 시작, 정리된 모듈부터 strict 승격 |
|
||||
| **1. 데드코드** | 자동수정→vulture 베이스라인→whitelist→knip 순. 정의-참조 grep 대조, 생산-소비 경계 추적, 자산-로더 대조 | ruff(F401/F811/F841 `--fix`), vulture(`--min-confidence 80`), knip, madge, coverage | 문자열 동적참조(`getattr`/라우트 문자열) 수동 재확인, FastAPI 핸들러·Pydantic 모델은 whitelist/`--ignore-decorators`로 오탐 차단, 모듈 단위 잘게 |
|
||||
| **2. 파라미터 객체화** | 임계 초과 함수 AST 전수→데이터 클럼프 식별→호출부 출처 추적→불변값은 frozen dataclass(slots)·HTTP 경계는 pydantic·키 묶음은 TypedDict | ruff PLR0913/PLR0917, pylint R0913, AST 커스텀 스캐너, dataclasses, pydantic, radon | 시그니처 변경=광범위 diff → 순수 리팩토링 단독 PR, 기존 테스트(`test_orchestrator_masking` 등) 선갱신 |
|
||||
| **3. 중복 통합** | 클론 유형(Type-1/2/3) 분류→호출 그래프 공통화→포맷/스토어 헬퍼 통합. 통합 중 숨은 의미 불일치(stage 라벨, seq base) 노출·일원화 | jscpd(`--min-tokens 50`), pylint duplicate-code, semgrep, radon | rule of three(3회 전 추출 보류), 우연한 유사는 건드리지 않음, 골든 테스트(`test_session_turn_persistence`, `test_voice_ws`)로 동등성 확인 |
|
||||
| **4. SSOT화** | 계약-우선: 도메인 enum은 백엔드 Python을 SoT로, 프론트 TS는 OpenAPI에서 생성(수기 미러 금지). DTO 미러부터 자동생성 전환→enum 통합→상수 모듈 | openapi-typescript, datamodel-code-generator, StrEnum, ts-prune, schemathesis, pytest 스냅샷(openapi.json 해시) | 타입만 먼저(런타임 무변경)라 저위험, CI에 "재생성 후 git diff 없음" 게이트, 컴파일 에러=드리프트 리포트이므로 끄지 말 것 |
|
||||
| **5. 추상화/패턴** | 결합도 핫스팟 식별→변하는 축(provider/tier/theory_mode) 분리(Strategy+Protocol)→영속성 이중화 Repository로 통합→파이프라인 Chain of Responsibility로 표면화 | radon cc/mi, import-linter/pydeps, mypy(Protocol 검증), xenon(복잡도 CI 게이트) | ABC보다 `typing.Protocol` 선호, "변경 잦고 복잡하고 테스트 있는" 모듈에만(over-engineering 경계), characterization test 선행 |
|
||||
| **6. 서비스 최적화** | 정적 게이트(mypy strict 점진, ruff ASYNC/PERF) 후 측정 기반 튜닝. N+1 제거(JOIN/배치), 불변값 캐시, 이벤트루프 블로킹 오프로딩 | ruff `--select ASYNC`, py-spy/pyinstrument, asyncpg EXPLAIN ANALYZE, rollup-plugin-visualizer | before/after 벤치마크 필수(수치 없는 최적화 금지), e2e(`voice-success.spec.ts`)가 회귀망 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 구체 타깃 목록 (차원별 우선순위)
|
||||
|
||||
### 3.1 데드코드 · 미사용 export · 고아 자산
|
||||
|
||||
| 우선 | Location | 이슈 | 심각도 | 권고 |
|
||||
|---|---|---|---|---|
|
||||
| 1 | `services/orchestrator.py:41,185,221-225,259,324-326,401` | `LogHook` 타입 + `log_hook` 파라미터 + 두 if 분기 + `__all__` export가 완전 데드코드. caller 0건(`grep 'log_hook='` → 0), 두 호출부(`sessions.py:916`, `voice.py:307`)가 모두 생략 → 항상 None 도달불가 | medium | 타입·파라미터·두 분기·export 제거. 턴 로깅이 필요하면 라우트에서 배선, 아니면 확장점 전체 삭제 |
|
||||
| 2 | `routes/voice.py:307` | 음성 라우트가 `eval_hook` 미주입 → 음성 턴 항상 `evaluation=None`. 텍스트(`sessions.py:916`)는 주입하는 비대칭. (orchestrator 분기 자체는 텍스트 경로로 도달하므로 데드 아님) | medium | 음성 턴도 평가 대상이면 `eval_hook=evaluator.make_eval_hook(engine_client)` 추가. 의도적 제외면 사유 주석 명시 |
|
||||
| 3 | 서버 `services/voice.py:223,132`·`routes/voice.py:378` / 프론트 `Session.tsx:913-1001,58-68` | 서버 립싱크 RMS 힌트(`tts_chunk.rms/seq`)가 프론트에서 완전 dead. `onmessage`가 해당 type 미처리, `VoiceEvent` 인터페이스에 필드 선언조차 없음. 실제 립싱크는 클라이언트 `AnalyserNode`가 자체 계산 → 두 RMS 산출 공존, 서버 쪽 폐기 | medium | 서버 RMS 미사용이면 `estimate_chunk_rms`·`TTSChunk.rms/seq`·`tts_chunk` 메타 제거(서버 CPU·프로토콜 cruft 제거). 또는 클라이언트 경로 제거 후 서버 힌트로 일원화 |
|
||||
| 4 | `data/personas/P4~P7.json` (+`services/persona.py:383`) | 백엔드가 P4~P7 JSON을 전혀 로드 안 함. 런타임 시드는 `SEED_PERSONAS={P1,P2,P3}` 하드코딩이 전부, `data/personas` 읽는 로더 0건. 완성된 카드·아바타 자산이 있어도 학습자가 내담자로 인스턴스화 불가 | medium | 사용 의도면 로더/마이그레이션 추가 또는 `SEED_PERSONAS` 편입, 미사용이면 삭제. 프론트 `live2dModel.ts`의 P4~P7 정의와 정합 필요 |
|
||||
| 5 | `public/live2d/personas/p4~p7/**` (+`dist` 미러) | Live2D 모델 자산이 런타임 fetch 안 됨. 실제 아바타는 SVG 컴포넌트로 렌더, `live2dModel3Path()`가 만든 URL은 `data-live2d-model-url` 속성 문자열로만 노출(fetch/cubism/pixi grep → 0). e2e도 속성 문자열만 어서션 | medium | Live2D 제거 기조(최근 커밋)와 맞춰 디스크 자산 트리 삭제 또는 빌드 제외. 남길 경우 실제 로더 부착 |
|
||||
|
||||
### 3.2 과다 파라미터 함수 → 파라미터 객체화
|
||||
|
||||
| 우선 | Location | 이슈 | 심각도 | 권고 |
|
||||
|---|---|---|---|---|
|
||||
| 1 | `services/state_machine.py:234 init_state` (+`:130`,`:164`) | 5개 키워드 중 앞 3~4개가 데이터 클럼프. 모든 호출부(5곳)가 `card.base_resistance()`/`unlock_rate()`/`decay_floor()`/`ideation_baseline()`를 1:1 분해. 트리오가 `compute_effective_openness`·`evolve`에도 중복 | high | frozen `OpennessParams` + `PersonaCard.openness_params()` 한 메서드. 5개 call-site의 4줄 분해가 1줄로 축약 |
|
||||
| 2 | `services/orchestrator.py:97 prepare_turn` / `persona.py:147 build_turn_messages` | `prepare_turn` 10개 키워드 최다. `recall_summary/pinned_facts/recent_turns/kb_behavior_cues` 4개가 `build_turn_messages`에도 재등장. 호출부는 이미 `memory.RecallContext`에서 풀어헤침 | high | frozen `TurnMemory`(+`from_recall` 팩토리). prepare_turn 10→6, 두 곳 클럼프 단일 타입 통일 |
|
||||
| 3 | `auth_sessions.py:408,371,516` | `upsert_managed_user`·`_memory_upsert_managed_user`가 7개 키워드를 글자 그대로 중복 선언, `update_managed_user`가 부분집합 5개 공유 → 변경 시 세 곳 동기화 | high | pydantic `ManagedUserInput`으로 묶어 DB/메모리 두 구현이 동일 입력 객체 수신 |
|
||||
| 4 | `routes/voice.py:213,273` | `_handle_utterance`(9kw)·`_run_turn_and_speak`(8kw)가 프로소디 메타(`audio_ref/silence_ms/speech_rate/barge_in`)를 WS→...→`TurnRecord`까지 3단계 전달 | medium | frozen `ProsodyMeta`로 캡슐화, 타이밍 계산 결과 묶음 |
|
||||
| 5 | `store.py:26 TurnRecord` (15필드) | LLM 사용량(`llm_provider/model/tokens_*/cost_usd`)과 음성 프로소디가 상호배타 두 묶음인데 평면 나열 → 생성부마다 한쪽만 채우고 나머지 None | medium | 코어 유지 + `usage: LlmUsage\|None`·`prosody: ProsodyMeta\|None` 중첩, voice `ProsodyMeta`와 타입 공유 |
|
||||
| 6 | `session_persistence.py:224 save_session_evaluation` | 8개 키워드 중 6개가 진입 직후 `record` dict로 재조립(235~242행) → 평면화가 즉시 dict화로 무효 | medium | `SessionEvaluationRecord` 정의 후 `record:` 단일 인자로 축약 |
|
||||
| 7 | `services/memory.py:117 make_carry_over` | 7개 키워드 중 5개가 함수 내부에서 그대로 `CompressionJob` 생성자에 전달 → 이미 존재하는 객체를 풀었다 재조립 | medium | `job: CompressionJob` 직접 수신, 7→3 파라미터 |
|
||||
| 8 | `session_persistence.py:377 create_session` / `store.py:87 SessionStore.create` | DB판(8kw)·in-proc판(6kw)이 세션 부트스트랩 묶음 중복. `persona_id/persona_version`은 항상 함께 쓰이는 핀 쌍 | low | `PersonaPin` 값객체 + `SessionBootstrap`로 두 구현 공통 입력 정렬 |
|
||||
| 9 | `services/evaluator.py:664,425` | `evaluate_session`(7kw)이 5개를 `build_deep_messages`(5kw)로 거의 그대로 위임 | low | frozen `DeepEvalRequest`로 통일, distribution은 req에서 파생 |
|
||||
| — | `apps/web/src/**` | TS 전수 스캔 결과 위치 파라미터 5개 이상 0건. 최대 `smoothMouthFromRMS` 4개로 임계 미만, 컴포넌트는 이미 props 객체 사용 | low | 현행 유지. 회귀 방지로 typescript-eslint `max-params`·ruff PLR0913만 CI 추가 |
|
||||
|
||||
### 3.3 중복 · 유사 로직
|
||||
|
||||
| 우선 | Location | 이슈 | 심각도 | 권고 |
|
||||
|---|---|---|---|---|
|
||||
| 1 | `voice.py:_append_voice_turn(409-419)`/`_update_voice_state(422-435)` ↔ `sessions.py:_append_session_turn(241-251)`/`_update_session_state(254-267)` | Type-2 완전 중복. 영속화 시도→성공 시 갱신+put→실패 시 폴백 4단계가 두 벌, 유일 차이는 로그 문자열뿐. 정책 변경 시 두 곳 동기화 필요 | high | `persist_turn(sess, turn, *, channel)`·`persist_state(...)`로 추출, `channel`만 파라미터화 |
|
||||
| 2 | `sessions.py:submit_turn(850-906)`,`stream_turn done(941-969)` ↔ `voice.py:_run_turn_and_speak(293-347)` | Type-3 핵심 중복. 동일 파이프라인 3복제 + **stage 표기 불일치**(`sessions`는 한글 `_stage_label`, `voice`는 enum `.value` 원시값) → 같은 turns 테이블에 채널별 다른 표기 | high | `record_completed_turn(sess, ctx, result, *, channel, audio_meta=None)` 추출, stage 규칙 단일화, 음성 필드는 optional `audio_meta` 흡수 |
|
||||
| 3 | `voice.py:_load_voice_session(391-406)` ↔ `sessions.py:_load_session_or_404(221-238)` | Type-3 중복. 로드→put→폴백→오너십→ended 검증 동일, 차이는 전달 방식(HTTPException raise vs 튜플 반환)과 ended 처리(`allow_ended` vs 항상 거부). IDOR/권한 변경 시 두 곳 동기화 | high | 공통 `load_owned_session(...) -> tuple[..., LoadError\|None]` 코어 + sessions는 예외 매핑 얇은 래퍼 |
|
||||
| 4 | `sessions.py:_generate_and_save_session_evaluation(489-533)` ↔ `eval.py:reevaluate_session(97-135)` | Type-3 + **seq base 불일치**: `sessions.py:494`는 1-기준 강제 덮어쓰기, `eval.py:101`은 0-기준 `setdefault` → 동일 데이터가 평가 프롬프트에 1-base/0-base로 다르게 입력 | medium | `enrich_masked_turns`(seq 규칙 1개 고정) + `run_and_persist_session_evaluation` 추출, 에러 전송 정책만 차이로 |
|
||||
| 5 | `teacher.py:_iso(48-51)` ↔ `sessions.py:_iso(286-289)` | Type-1 바이트 동일 함수. `_summary`/`_learner_summary`도 동형 구조인데 stage 표기 또 불일치(`.value` vs `_stage_label`) | medium | `_iso`(+시간 포맷 순수함수)를 `app/timefmt.py`로 단일 정의, `build_session_summary(sess, *, viewer_role)`로 통합 |
|
||||
| 6 | `store.py:recent_turns(68-71)` ↔ `masked_turns(76-78)` | Type-2. 두 본문 사실상 동일, `recent_turns`는 `[-k:]` 슬라이스만 차이 | low | `recent_turns(k,...)=self.masked_turns(...)[-k:]` 위임, `{'speaker','text'}` 직렬화는 `_to_masked_dict` 헬퍼 |
|
||||
| 7 | 내담자 `TurnRecord` 생성 3곳(`sessions.py:884-895,957-968`·`voice.py:334-345`) | Type-2 반복. 텔레메트리 매핑 3복제 + `stream_turn`은 `ev.data` dict 재파싱(또 다른 변형) | low | `TurnRecord.from_engine_result`/`from_learner` 팩토리 또는 `record_completed_turn`에 흡수 |
|
||||
| 8 | `teacher.py:60-61`·`sessions.py:544-545,310-311` | Type-2. 턴 카운팅·speaker 정규화(`counselor→learner`)가 여러 곳 산재 | low | `count_turns(turns)`·`display_speaker(speaker)` 순수 헬퍼로 일괄 치환 |
|
||||
|
||||
### 3.4 규약 · 인터페이스 SSOT화
|
||||
|
||||
| 우선 | Location | 이슈 | 심각도 | 권고 |
|
||||
|---|---|---|---|---|
|
||||
| 1 | `sessions.py:40-192` ↔ `web/src/lib/api.ts:205-355` | 세션/리뷰 응답 ~14개 pydantic 모델이 TS에 수기 미러. **드리프트 실증**: 백엔드 `TurnResponse.crisis_kind`·`SessionEndResponse.end_state`가 TS에 누락 | high | openapi-typescript 도입(openapi.json→`api.gen.ts` 생성, `api.ts`는 re-export만), CI "재생성 후 diff 없음" 게이트. 즉시조치로 누락 필드부터 보충 |
|
||||
| 2 | `state_machine.py:23` ↔ `taxonomy.py:35` | `Stage(str,Enum)`가 두 모듈에 글자 동일 중복(`라포/탐색/개입/정리`). 주석이 중복 자인. `taxonomy.Stage`는 외부 import 0건이라 "단일 코드 원천" 주장과 모순 | high | Stage를 한 곳에만 정의, `taxonomy.py`는 re-export. 미사용 확인 후 삭제. 라벨·phase_key는 enum 메서드로 흡수 |
|
||||
| 3 | `taxonomy.py:47 Speaker` ↔ `store.py:30` ↔ 산재 문자열 | 화자 어휘 4종 분열: enum(import 0건), `TurnRecord.speaker:str`, `'learner' if speaker=='counselor'` 삼항 다수 산재, DB용 `human_learner/client_ai` 즉석 매핑 | high | `taxonomy.Speaker`를 실제 SoT로 승격, `to_api_speaker`/`to_db_actor` 단일 헬퍼로 변환 캡슐화, 삼항식 치환 |
|
||||
| 4 | `deps.py:22 AIView` ↔ 6+곳(`engine_client/kb/rag/store`+SQL) | AI-view 삼중쌍 `client/counselor/evaluator`가 6+곳 독립 정의, `rag.py:48` docstring이 수기 동기화 자인 | medium | `domain/ai_view.py`에 단일 `AIView(StrEnum)`+`AI_VIEW_VALUES`, SQL 기본값도 상수 바인딩 |
|
||||
| 5 | `deps.py:16 Role` ↔ 백엔드 4곳+프론트 3곳 | 역할 `learner/teacher/admin` 중복 + app↔db 매핑(`teacher↔instructor`) 3중 복제 | medium | `Role` enum SoT, app↔db 매핑 단일 dict, 프론트는 OpenAPI 생성 타입 |
|
||||
| 6 | `personas.py:24-47` ↔ `api.ts:176-203` | `PersonaSummary` 수기 미러. `difficulty`가 백엔드 str ↔ TS 좁힘, `PersonaReviewStatus` 이중 정의 | medium | OpenAPI 생성으로 자동 해소, `difficulty`는 백엔드에서 `Literal` 좁혀 SoT 고정 |
|
||||
| 7 | `sessions.py:198-213 (_PHASE_KEY_BY_LABEL,_stage_label)` | Stage 영문→한글, 한글→phase_key 역매핑이 enum과 별개 dict 리터럴로 하드코딩 | medium | `_stage_label`은 `stage.value` 직접 사용, `phase_key`는 Stage 프로퍼티로 단일 출처화 |
|
||||
| 8 | `orchestrator.py:251` ↔ `sessions.py:937-979` ↔ `api.ts:359` | SSE 이벤트명(`token/done/safety/ping/error`)이 상수 없이 3계층 산재, 집합도 미세 불일치(`safety` vs `ping`) | low | 백엔드 `SSE_EVENTS` 단일 정의, 프론트는 공유 const 객체로 매직 문자열 제거 |
|
||||
|
||||
### 3.5 추상화 · 디자인 패턴 · 서비스 최적화
|
||||
|
||||
| 우선 | Location | 이슈 | 심각도 | 권고 |
|
||||
|---|---|---|---|---|
|
||||
| 1 | `engine_gateway/gateway.py:424-433,379-397` + `engine_client.py:71-203` | tier 라우팅 dead-contract. `GwGenerateReq.tier`를 `_resolve_session`이 미사용, 항상 `DEFAULT_MODEL`+`claude_cli` 반환. tier별 비용/지연 최적화(평가=Opus, fast=Haiku) 미구현 | high | `EngineProvider(Protocol)` + 레지스트리, `TierModelPolicy`(Strategy)로 tier→(provider,model) 매핑. 우선 게이트웨이에 정책 테이블만 추가해도 해소 |
|
||||
| 2 | `persona.py:147` ↔ `orchestrator.py:97` ↔ `evaluator.py:367-372` | `theory_mode`(humanistic/cbt/integrative)가 죽은 전략 축. DB까지 전파되나 `build_turn_messages`가 인자로 받지도 않아 프롬프트가 이론별 무변화. evaluator는 문자열로만 끼움. `_theory_mode`가 `persona.theory_target` 참조로 소스 이원화 | high | `TheoryModeStrategy(Protocol)` 도입, 단일 소스(`session.theory_mode`)로 통일. 1차로 이론별 L6 발화지시+평가 기법 가중만 분기해도 효과 |
|
||||
| 3 | 라우트 3곳 영속성 분기(`sessions.py:221-267`·`voice.py:391-419`·`eval.py:58-66`) | DB-or-inproc fallback이 3곳 거의 동일 복제. **표류 실재**: voice `_run_turn_and_speak`가 빈 `RecallContext()`를 매 턴 생성 → 회상/메모리 주입 누락 | high | `SessionRepository(Protocol){load,append_turn,update_state,end,list}`로 정책 1곳 캡슐화, 라우트는 도메인 호출만. RecallContext를 repo/세션 컨텍스트에 귀속 |
|
||||
| 4 | `evaluator.py:738-750 make_eval_hook` ↔ `sessions.py:862-863,936` | DI seam이 정의·export·테스트만 있고 호출 0건. fast-loop 평가가 운영 경로에서 미실행, 평가는 회기말 deep-loop에만 의존 | high | (A) `eval_hook`/`log_hook` 실제 배선해 fast-loop 켜기, 또는 (B) fast-loop 미사용이면 경로 제거. vulture로 사후 참조 0 재확인 |
|
||||
| 5 | `session_persistence.py:695-720 list_sessions` | N+1. 세션 목록(최대 100) 순회마다 `session_state` fetchrow+`turns` fetch 개별 실행 → 1+2N 왕복, NAS Postgres 지연 곱 | medium | `session_state`는 LEFT JOIN, `turns`는 `WHERE session_id = ANY($ids)` 단일 fetch 후 그룹핑 또는 `json_agg`. 목록이 본문 불필요하면 count 집계만 |
|
||||
| 6 | `orchestrator.py:121-123,168-174` ↔ `store.py:68-71` | 턴마다 불필요 재마스킹. 불변 `recall_summary/pinned_facts`를 매 턴 Presidio 재실행 + `recent_turns`는 이미 마스킹된 `text_masked`를 또 마스킹(이중) | medium | 불변값 1회 마스킹 후 캐시, `_mask_recent_turns` 제거(또는 마스킹 불변식을 타입으로 명확화). `test_orchestrator_masking`으로 가드 |
|
||||
| 7 | `guardrail.py:75-100` (`prepare_turn` 호출) · `voice.py:223-246` | 이벤트 루프 동기 블로킹. async 핸들러 안에서 `mask_pii`(Presidio CPU 바운드) 동기 실행, `estimate_chunk_rms`도 async 제너레이터 안 동기 바이트 루프 | medium | `anyio.to_thread.run_sync`로 오프로딩, Presidio 엔진 startup 워밍업. RMS는 옵션화(`VOICE_SERVER_RMS=off`)로 핫패스 제거 가능 |
|
||||
| 8 | `orchestrator.py:180-241,255-346` | 위기·가드레일·평가 파이프라인이 한 함수 절차적 if 블록. 재생성 루프·신규 안전단계 삽입 시 본문 수술 필요 | medium | Chain of Responsibility: `TurnStage(Protocol)` 핸들러 리스트 순회. 단락·재시도·삽입을 핸들러 추가/순서로 처리 |
|
||||
| 9 | `sessions.py:536-539,1007` | 회기말 평가가 `create_task` 결과 미보관 fire-and-forget → GC로 조용히 취소/예외 유실 가능 | low | 모듈 set에 add + `add_done_callback(discard)`로 강참조, 예외 로깅 콜백. 견고화는 작업 큐로 이전 |
|
||||
| 10 | `persona.py:99-111 _format_openness_directive` | 0.2/0.4/0.65/0.85 임계 if/elif 사다리 하드코딩 → 밴드 추가/튜닝/A/B 시 본문 수정(OCP 위반) | low | 테이블 주도((threshold, directive) 리스트)로 치환, TheoryModeStrategy가 이론별 오버라이드 확장 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 적용 순서 · 리스크 · 회귀 방지
|
||||
|
||||
순서 논리: **삭제(되돌리기 쉬움) → 자동화(생성) → 통합 → 재구성(시그니처) → 재설계(구조) → 튜닝.** 단계마다 리스크와 diff 범위가 커지고, 각 단계는 다음 단계의 전제다.
|
||||
|
||||
```
|
||||
0. 측정 도구 고정 + 베이스라인 [리스크 0, 모든 것의 전제]
|
||||
1. 데드코드 제거 [저위험·고효과, 표면 축소]
|
||||
2. 계약 SSOT화 (OpenAPI→TS) [구조적 최고가치, 타입만이라 저위험]
|
||||
3. 중복 통합 (rule of three 준수) [중위험, drift 잠재버그 제거]
|
||||
4. 파라미터 객체화 [중위험, 시그니처 변경]
|
||||
5. 추상화/패턴 (복잡 핫스팟만) [고위험, 테스트 선행 필수]
|
||||
6. 서비스 최적화 [최후, 측정 기반]
|
||||
```
|
||||
|
||||
| 단계 | 무엇부터 | 리스크 | 회귀 방지 테스트 |
|
||||
|---|---|---|---|
|
||||
| 0 | `pyproject.toml`·`knip.json` 신설, `quality-baseline.ps1` | 없음 | — (수치 스냅샷만) |
|
||||
| 1 | `ruff --fix`(즉시) → knip `files`+`dependencies` → vulture 핫스팟(`rag.py`/`evaluator.py`/`auth_sessions.py`). Live2D 잔재 1순위 | 저 | 삭제 PR은 모듈 단위, 문자열 동적참조 수동 grep, FastAPI/Pydantic은 whitelist |
|
||||
| 2 | DTO 미러 자동생성(누락 `crisis_kind`/`end_state`부터) → Stage/Speaker/Role enum 통합 | 저(타입만) | `tsc -b` 컴파일 통과 = 드리프트 해소, CI openapi.json diff 게이트, `test_runtime_policy` |
|
||||
| 3 | `record_completed_turn`/`load_owned_session`/`persist_turn` 추출(턴 파이프라인), `_iso` 통합 | 중 | `test_session_turn_persistence`, `test_voice_ws`를 기준선으로 추출 전후 동등성. stage/seq 불일치는 통합 중 단일 규칙 확정 |
|
||||
| 4 | `OpennessParams`(저위험 클럼프) → `TurnMemory` → `ManagedUserInput`. 호출처 적은 것부터 | 중(시그니처) | `test_orchestrator_masking`, `test_state_machine_resistance`, `test_rbac_idor` 선갱신. 순수 리팩토링 단독 PR |
|
||||
| 5 | `SessionRepository`(영속성 통합) → `TierModelPolicy`(dead-contract) → `TheoryModeStrategy` → 파이프라인 CoR. radon 핫스팟 순 | 고 | characterization test 선행 필수. `test_gateway_model`, `test_state_machine_resistance` |
|
||||
| 6 | `list_sessions` N+1 → 불변값 마스킹 캐시 → Presidio/RMS 오프로딩 | 최후·측정 | before/after 벤치(py-spy/pyinstrument, EXPLAIN ANALYZE), e2e `voice-success.spec.ts` |
|
||||
|
||||
---
|
||||
|
||||
## 5. 즉시 안전 적용 vs 설계 변경 필요
|
||||
|
||||
### A. 즉시 안전 적용 (저위험 · 단독 PR · 되돌리기 쉬움)
|
||||
|
||||
- **`ruff --fix`(F401/F811/F841)** 미사용 import·재정의·지역변수 자동 정리. 리뷰 부담 거의 0.
|
||||
- **명백한 데드코드 삭제**: `LogHook` 타입·`log_hook` 파라미터·두 분기·`__all__` export(caller 0건 입증됨).
|
||||
- **고아 자산 제거**: Live2D 모델 트리(`public/live2d/personas/**` + dist 미러, fetch 주체 0건), 사용 계획 없는 `data/personas/P4~P7.json` — 단, 프론트 `live2dModel.ts` 정의와의 정합 확인 후.
|
||||
- **Type-1 동일 함수 통합**: `_iso`를 `app/timefmt.py`로 단일 정의.
|
||||
- **음성 `eval_hook` 누락 보충**(`voice.py:307`): 텍스트 경로와 동일한 한 줄 주입(단, 음성 턴 평가가 제품 의도인지 먼저 확인 — 의도적 제외라면 주석만).
|
||||
- **DTO 드리프트 즉시 보충**: `crisis_kind`/`end_state` 등 누락 필드를 TS에 수동 추가(자동생성 전 최소 조치).
|
||||
- **회귀 방지 린트 게이트 추가**(동작 무변경): typescript-eslint `max-params`, ruff `PLR0913`/`ASYNC`/`PERF`를 `--exit-zero`로.
|
||||
|
||||
### B. 설계 변경 필요 (테스트 선행 · 단계적 · 동작 변경 동반 가능)
|
||||
|
||||
- **턴 처리 파이프라인 통합**(3.3-1~3): `record_completed_turn`/`load_owned_session`/`SessionRepository` 추출. stage 표기·seq base·RecallContext 주입을 통합 과정에서 단일 규칙으로 확정 — **동작 변경(채널 간 일관성 회복)을 동반**하므로 골든 테스트 기준선 필수.
|
||||
- **계약 자동생성 파이프라인**(3.4-1): openapi-typescript 도입 + CI diff 게이트. 인프라 변경이라 단독 단계.
|
||||
- **enum SSOT 통합**(3.4-2~5): Stage/Speaker/AIView/Role를 단일 모듈로, 변환을 헬퍼로 캡슐화. import 0건인 `taxonomy.Stage` 등은 vulture 확인 후 삭제 — 광범위 치환이라 모듈별 PR.
|
||||
- **파라미터 객체화 전반**(3.2): frozen dataclass/pydantic 도입은 시그니처 변경 = 광범위 diff. 테스트 선갱신 후 순수 리팩토링 단독 PR.
|
||||
- **전략/패턴 도입**(3.5-1~2,8): `TierModelPolicy`·`TheoryModeStrategy`·파이프라인 CoR. dead-contract를 실제 기능으로 살리는 것이므로 **신규 동작 추가**이고, `theory_mode`는 프롬프트/평가 결과를 바꾸므로 회귀·품질 검증 필요.
|
||||
- **DI seam 결론**(3.5-4): `make_eval_hook` fast-loop을 켤지(동작 추가) 제거할지(표면 축소) 제품 결정 선행. 어느 쪽이든 vulture로 사후 참조 0 재확인.
|
||||
- **성능 최적화**(3.5-5~7): N+1 JOIN화, 마스킹 캐시, Presidio/RMS 오프로딩. 정확성 의미가 바뀔 수 있어(마스킹 불변식) before/after 벤치와 e2e 회귀망 위에서만.
|
||||
|
||||
> **단정 제한 주의:** 본 보고서의 "데드"·"미사용" 판정은 제공된 분석의 grep/AST 근거에 기반하나, 문자열 동적참조(`getattr`/라우트 문자열/리플렉션)와 멀티라인 호출은 정적 도구가 놓칠 수 있다. 삭제 전 해당 모듈에 vulture whitelist·knip entry point 설정과 수동 "Find usages"를 반드시 교차검증할 것. 또한 일부 항목(P4~P7 사용 여부, 음성 턴 평가 포함 여부, fast-loop 사용 여부)은 **제품 의도 확인이 선행**되어야 "삭제 vs 배선"이 결정된다.
|
||||
57
docs/ops/handoff-avatar-seoyeon-2026-06-27.md
Normal file
57
docs/ops/handoff-avatar-seoyeon-2026-06-27.md
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
# 서연(P1) 아바타 작업 핸드오프 — 2026-06-27
|
||||
|
||||
> 다른 세션에서 이어서 작업하기 위한 인계. imagegen(gpt-image-2)+BiRefNet+파츠 분리 리깅 파이프라인 전체를 여기서 참조.
|
||||
> 상세 원칙은 `CLAUDE.md` §4.
|
||||
|
||||
---
|
||||
|
||||
## 0. 한 줄 요약
|
||||
파이프라인·렌더러·dev 미리보기까지 **완성**됐으나, 서연 아트가 **잘못된 참조(Image #1=긴 머리)**로 만들어져, **진짜 목표 Image #2(짧은 갈색 보브, 정면)**로 **재생성**이 남았다.
|
||||
|
||||
---
|
||||
|
||||
## 1. ⚠️ 다음 세션이 가장 먼저 할 일 (핵심)
|
||||
1. **목표 재확인**: 진짜 타겟은 **Image #2** = `D:\workspace\GLM\.claude-glm\image-cache\73cbbea0-5c27-4ced-91db-16d7a8df602f\2.png`
|
||||
(단일 캐릭터, **짧은 갈색 보브**, 정면 — 현재 서연의 긴 머리와 다름. 사용자가 "잉??" 한 원인).
|
||||
- 비전(analyze_image) 도구가 다중 패널/캐릭터를 자주 혼동했으므로, **재생성 전 사용자에게 Image #2 해석(짧은 보브, 의상 색 등)을 한 번 확인**할 것.
|
||||
2. **Image #2를 `-i` 참조로 재생성**하면 gpt-image-2가 캐릭터를 그대로 따라함(텍스트 묘사 의존 X). 아트 디렉션은 2.png에 맡긴다.
|
||||
|
||||
## 2. 완료된 것 (재사용 가능)
|
||||
- **이미지 생성**: `~/.codex/imagegen-headless/codex_imagegen.sh`(gpt-image-2, ChatGPT 구독). 검증 완료.
|
||||
- **누끼**: `object-separation` 스킬(BiRefNet). 알파 정제(임계치 35/205)까지 확인.
|
||||
- **파이프라인 스크립트**(`docs/avatar-art/seoyeon/`):
|
||||
- `prompt-base.txt` — base 생성 프롬프트(Image #1 기준; Image #2 재생성 시 -i 참조로 대체 가능)
|
||||
- `gen-variants.sh` — 표정 변주 7종(sad/tired/anxious/warm/startled/eyes-closed/speaking) 일괄 생성. base를 `-i`로.
|
||||
- `cut-publish.sh` — BiRefNet 누끼 → 게시 일괄
|
||||
- `publish.py` — 표준 캔버스(900×1125) 정규화 + 알파 정제. `python publish.py <컷아웃> <name>`
|
||||
- `make-parts.py` — 특징 영역(UPPERFACE/EYELID/MOUTH) 크롭+페더 파츠. **영역 상수 튜너 블럭**(상단).
|
||||
- **렌더러**: `apps/web/src/components/avatar/RasterBust.tsx`
|
||||
- 파츠 분리: `base` + `upperface-<표정>`(눈썹+눈) + `eyelid-closed`(깜빡임, 1-blink) + `mouth-<표정>` + `mouth-open`(립싱크, RMS).
|
||||
- 28표정→클러스터 매핑(`VARIANT_FOR_EXPRESSION`) 내장.
|
||||
- **연결 배선**:
|
||||
- `persona.ts`: `AvatarPersona.rasterArtSet?: string` 추가됨.
|
||||
- `Session.tsx`: `PERSONA_AVATAR_LOOKS.P1 = { rasterArtSet: "seoyeon", expressionBias: "sad", ... }` + avatar 객체에 `rasterArtSet` 전달.
|
||||
- `ClientAvatar.tsx`: `useRaster` 분기(rasterArtSet 있으면 `RasterBust`, 없으면 기존 SVG). `data-render-mode` 속성 추가.
|
||||
- `App.tsx`: `/dev/avatar-preview` 라우트(인증 없음).
|
||||
- **dev 미리보기**: `apps/web/src/pages/AvatarPreview.tsx`(서연 220px, speaking/sad·idle/sad·listening/warm·thinking/anxious).
|
||||
- **스크린샷 도구**: `apps/web/scripts/avatar-shot.mjs` (`BASE_URL=http://localhost:<port> node ...`).
|
||||
- **현재 에셋(Image #1=긴 머리 버전)**: `apps/web/public/avatar/seoyeon/{neutral,sad,tired,anxious,warm,startled,eyes-closed,speaking}.png` + `parts/`. → Image #2 재생성 시 덮어쓰기.
|
||||
|
||||
## 3. 남은 작업 (순서대로)
|
||||
1. Image #2(2.png) 해석 사용자 확인 → ASCII 이름으로 복사: `cp "<2.png 경로>" docs/avatar-art/seoyeon/ref-image2.png`.
|
||||
2. **base 재생성**(검증용 1장):
|
||||
`codex_imagegen.sh --out base-v2.png --size 1024x1280 --quality high -i ref-image2.png "Use the supplied reference as the exact character. Same character, head-and-shoulders bust, perfectly front-facing, symmetrical, neutral calm expression, flat solid light gray #C9CDD2 background, no gradient/shadow/scenery, semi-realistic soft anime. No text/watermark/hands. Bangs above eyes."`
|
||||
→ 결과(base-v2.png)를 사용자에게 보여주고 Image #2 일치 확인.
|
||||
3. **변주 재생성**: `gen-variants.sh` 의 `BASE` 를 `base-v2.png`로 바꾸고 실행(동일 7종). → `cut-publish.sh` → `make-parts.py`.
|
||||
4. **검증**: dev 서버(5173)의 `/dev/avatar-preview` 를 `avatar-shot.mjs`로 캡처 → 사용자 확인. 파츠 정렬이 틀리면 `make-parts.py` 영역 상수(UPPERFACE/EYELID/MOUTH) 튜닝 후 재생성.
|
||||
5. **검증 게이트**(CLAUDE.md §2): `cd apps/web && npm run typecheck`. 레이아웃 변경 시 `e2e/session-layout.spec.ts`(8/8)+`e2e/layout-visual-gate.spec.ts`(7/7) — **web+api(+DB) 스택 필요**.
|
||||
|
||||
## 4. 환경 메모
|
||||
- **dev 서버**: 5173 단일 인스턴스로 띄움(백그라운드). 끊기면 `cd apps/web && npm run dev -- --port 5173 --strictPort`. (5173/5174 중복 인스턴스가 "깨진 화면" 원인이었음 — 항상 5173 단일로.)
|
||||
- codex CLI 0.142.2, auth_mode=chatgpt(구독). object-separation venv: `~/.venvs/object-separation` (CPU-only BiRefNet).
|
||||
- 서연=P1은 API 시드(`apps/api/app/services/persona.py`). 웹엔 P1이 `PERSONA_AVATAR_LOOKS`에 있고 rasterArtSet="seoyeon".
|
||||
|
||||
## 5. 미해결/결정 필요
|
||||
- Image #2 캐릭터의 **의상 색/추가 디테일** (비전 도구 불안정 → 사용자 확인 필요).
|
||||
- 파츠 분리 리깅 vs 통째 변주 크로스페이드 — 사용자가 "파츠 분리"를 원했으므로 현행(파츠 분리) 유지하되, 결과가 어색하면 통째 변주 방식(`RasterBust` 이전 버전, git 히스토리 참조)으로 롤백 검토.
|
||||
- 실제 Live2D Cubism(.moc3) 도입 여부는 소유자 결정(편집기 저작 필요).
|
||||
203
docs/ops/hanshin-data-governance-gate.md
Normal file
203
docs/ops/hanshin-data-governance-gate.md
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
# Hanshin Data Governance and SSO Gate
|
||||
|
||||
## Purpose
|
||||
|
||||
This is the Phase 0/P1 entry evidence gate for Hanshin data access, consent scope,
|
||||
and institutional SSO. It is an operations checklist, not legal advice and not an
|
||||
approval record.
|
||||
|
||||
Do not mark this gate complete from assumptions. Every required row needs an owner,
|
||||
evidence path, reviewer, date, and an explicit decision.
|
||||
|
||||
Primary evidence packet:
|
||||
|
||||
- `00-governance/hanshin-official-inquiry.pdf`
|
||||
- `00-governance/hanshin-response.pdf`
|
||||
- `00-governance/consent-scope-matrix.md`
|
||||
- `00-governance/sso-claim-map.md`
|
||||
- `00-governance/privacy-review.md`
|
||||
|
||||
## Gate Rule
|
||||
|
||||
P1 work may proceed only as a technical dry run until these external facts are
|
||||
documented:
|
||||
|
||||
- Hanshin confirms the SSO protocol, test path, and released claims.
|
||||
- The data steward confirms what source data can be supplied and under what
|
||||
consent scope.
|
||||
- Minor/source-case data use has explicit lawful consent or a documented decision
|
||||
blocking that data from live use.
|
||||
- Privacy review covers storage, retention, withdrawal, third-party model transfer,
|
||||
and any overseas transfer of masked text.
|
||||
|
||||
## Artifact Owners
|
||||
|
||||
| Artifact | Required owner | Required reviewer | Evidence field | Status |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Official inquiry letter | Project operator | Data steward | Letter path, sent_at, recipient | Pending |
|
||||
| Hanshin written response | Hanshin contact | Project operator | Response path, received_at | Pending external evidence |
|
||||
| Consent scope matrix | Data steward | Legal/privacy reviewer | Matrix path, consent form versions | Pending |
|
||||
| SSO claim map | Auth owner | Hanshin IdP owner | Metadata path, sample claims, test result | Pending |
|
||||
| Privacy review | Legal/privacy reviewer | Data steward | Review path, decision, open issues | Pending |
|
||||
|
||||
## Official Inquiry Questions
|
||||
|
||||
Use these questions in the official inquiry packet. Keep answers as written evidence;
|
||||
do not paraphrase verbal answers as approval.
|
||||
|
||||
| Area | Question | Owner | Evidence required | Live gap if missing |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| SSO protocol | Does Hanshin require OIDC/OAuth, SAML 2.0, both, or neither for this pilot? | Auth owner | Written response plus IdP metadata or OIDC discovery URL | Cannot finalize provider implementation |
|
||||
| SSO test access | Is there a test tenant, test account, or staged IdP path? | Auth owner | Test account procedure without secrets in repo | Cannot prove live login flow |
|
||||
| Claim release | Which claims/attributes are released for learners, instructors, and admins? | Auth owner | Sample decoded claims or SAML attribute statement with values redacted | RBAC mapping remains speculative |
|
||||
| Identifier stability | Which claim is the stable user key across semesters and role changes? | Data steward | Written statement naming the claim | Account linking risk |
|
||||
| Role source | Where do instructor/admin roles come from: IdP groups, roster import, or manual approval? | Data steward | Role source decision and evidence path | Privilege model cannot go live |
|
||||
| Domain policy | Which email domains are eligible and how are alumni/contractors handled? | Data steward | Domain list and exception process | Admission policy incomplete |
|
||||
| Deprovisioning | How quickly are withdrawn or ineligible users removed from SSO/roster access? | Data steward | Deprovisioning SLA or operating procedure | Access removal evidence gap |
|
||||
| Source transcripts | How many additional transcripts or session records can Hanshin provide, and in what form? | Data steward | Dataset inventory, counts, date range, allowed fields | n=1 dataset risk remains |
|
||||
| Minor/source-case consent | Does existing consent cover AI learning, secondary processing, DB retention, and derived artifacts? | Legal/privacy reviewer | Consent form versions and review decision | Source-case data blocked |
|
||||
| Guardian consent | If source-case data includes minors, is guardian consent present for each requested use? | Legal/privacy reviewer | Consent receipt index, no identities in repo | Minor data blocked |
|
||||
| Raw data handling | May raw transcripts/audio be stored, or only masked/transformed text? | Data steward | Handling rule and storage location | Raw ingestion blocked |
|
||||
| Third-party inference | May masked text be sent to external model providers for feedback or evaluation? | Legal/privacy reviewer | Transfer review and provider list | External calls limited or blocked |
|
||||
| Overseas transfer | Are overseas transfer notices or separate consent required for masked text leaving Korea? | Legal/privacy reviewer | Review decision and notice/consent version | Overseas provider use blocked |
|
||||
| Retention | What retention, deletion, and archival periods apply to source data, pilot data, logs, and exports? | Data steward | Retention schedule | No live storage approval |
|
||||
| Withdrawal | What happens after participant or source-case consent withdrawal? | Data steward | Withdrawal runbook and limitation notes | Export/KPI use blocked |
|
||||
|
||||
## Consent Scope Checklist
|
||||
|
||||
Create `00-governance/consent-scope-matrix.md` with one row per consent form version
|
||||
and data source. Mark unknowns as `blocked`, not `approved`.
|
||||
|
||||
| Scope item | Required decision | Owner | Evidence | Current gate status |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Trainee account creation and login | Required for pilot user access | Data steward | Consent or participation notice version | Pending |
|
||||
| Session text storage | Required before storing trainee sessions | Legal/privacy reviewer | Consent text and retention rule | Pending |
|
||||
| Supervisor/instructor review | Required before instructor dashboard use | Data steward | Consent text and RBAC procedure | Pending |
|
||||
| KPI reporting | Required before pilot metrics | Data steward | Consent text and aggregation rule | Pending |
|
||||
| Recursive learning seed export | Separate opt-in or documented lawful basis required | Legal/privacy reviewer | Consent version and export limitation | Pending |
|
||||
| Source transcript reuse | Required before importing Hanshin source cases | Data steward | Source inventory and consent review | Pending |
|
||||
| Minor/source-case AI learning | Separate guardian/legal basis decision required | Legal/privacy reviewer | Consent receipt index or block decision | Pending external evidence |
|
||||
| Raw audio storage | Separate decision required; default blocked | Data steward | Audio retention and access control rule | Blocked until approved |
|
||||
| Third-party model processing | Required before sending text to any model provider | Legal/privacy reviewer | Provider/field list and transfer review | Pending |
|
||||
| Overseas transfer notice | Required if provider processing leaves Korea | Legal/privacy reviewer | Notice/consent version or legal memo | Pending external evidence |
|
||||
| Withdrawal after participation | Required before live pilot | Data steward | Withdrawal log procedure and limits | Pending |
|
||||
| Identity map handling | Required before roster import | Auth owner | Storage location, access owner, no repo storage | Pending |
|
||||
|
||||
Minimum fields for each matrix row:
|
||||
|
||||
```text
|
||||
source_or_consent_version:
|
||||
scope_item:
|
||||
decision: approved | blocked | needs_review
|
||||
allowed_data_fields:
|
||||
blocked_data_fields:
|
||||
retention_rule:
|
||||
withdrawal_rule:
|
||||
owner:
|
||||
reviewer:
|
||||
evidence_path:
|
||||
reviewed_at:
|
||||
open_issues:
|
||||
```
|
||||
|
||||
## SSO, OAuth, and SAML Claim Mapping
|
||||
|
||||
This table separates user login SSO from engine/provider OAuth tokens. Engine OAuth
|
||||
or CLI credentials are out of scope for institutional identity and must not be used
|
||||
as user identity evidence.
|
||||
|
||||
| App field | OIDC/OAuth claim | SAML attribute | Required | Owner | Evidence required | Gap if missing |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| `auth_subject` | `sub` | persistent `NameID` or configured immutable ID | Yes | Auth owner | Redacted sample claim and stability statement | Cannot link accounts safely |
|
||||
| `email` | `email` | `mail` or equivalent | Yes | Auth owner | Redacted sample claim | Cannot identify eligible domain |
|
||||
| `email_verified` | `email_verified` | IdP assurance statement or verified mail attribute | Yes for OIDC | Auth owner | Claim sample or IdP policy | Email trust unresolved |
|
||||
| `email_domain` | `hd` or parsed domain | derived from `mail` | Yes | Data steward | Domain eligibility list | Domain gate speculative |
|
||||
| `display_name` | `name` | `displayName` | Optional | Auth owner | Claim sample | UI may use fallback |
|
||||
| `given_name` | `given_name` | `givenName` | Optional | Auth owner | Claim sample | UI may use fallback |
|
||||
| `family_name` | `family_name` | `sn` | Optional | Auth owner | Claim sample | UI may use fallback |
|
||||
| `institution_user_id` | custom claim if available | `employeeNumber`, `studentNumber`, or equivalent | Preferred | Data steward | Attribute release approval | Roster matching weaker |
|
||||
| `role_source` | `groups`, custom role claim, or roster import | `memberOf`, eduPerson affiliation, or roster import | Yes | Data steward | Role mapping decision | RBAC cannot go live |
|
||||
| `cohort_or_department` | custom claim or roster import | department/cohort attribute or roster import | Preferred | Data steward | Mapping rule | Instructor scope incomplete |
|
||||
| `auth_method` | `acr`/`amr` if available | AuthnContextClassRef | Optional | Auth owner | IdP policy | Cannot assert MFA/assurance |
|
||||
| `session_expires_at` | `exp` plus app session TTL | SAML session index/session TTL | Yes | Auth owner | TTL policy and test result | Session risk unresolved |
|
||||
|
||||
Required SSO evidence files:
|
||||
|
||||
- IdP metadata or OIDC discovery document location.
|
||||
- SP/client configuration record with secrets redacted.
|
||||
- Redacted successful login claim sample for learner.
|
||||
- Redacted successful login claim sample for instructor/admin if roles are IdP-backed.
|
||||
- Negative authorization test showing an ineligible account is denied.
|
||||
- Logout/deprovisioning behavior note.
|
||||
|
||||
## Live and External Evidence Gaps
|
||||
|
||||
The following are not currently proven by repository code or local docs and must stay
|
||||
open until external evidence is attached:
|
||||
|
||||
| Gap | Blocks | Required close-out evidence |
|
||||
| --- | --- | --- |
|
||||
| Hanshin SSO protocol is not confirmed | Production SSO selection | Written response plus metadata/discovery URL |
|
||||
| Released SSO claims are not confirmed | RBAC and account linking | Redacted claim samples and mapping approval |
|
||||
| Instructor/admin role source is not confirmed | `/teach` and admin live access | Role source decision and authorization test |
|
||||
| Additional transcript supply is not confirmed | Dataset expansion beyond seed data | Dataset inventory with allowed fields and counts |
|
||||
| Minor/source-case consent scope is not confirmed | Any source-case import or recursive learning use | Consent review decision or explicit block decision |
|
||||
| Overseas/third-party model transfer review is not complete | External model calls with real data | Privacy review naming providers, fields, and transfer basis |
|
||||
| Withdrawal and retention rules are not signed | Pilot launch and export generation | Signed runbook, retention schedule, and withdrawal log path |
|
||||
|
||||
## Evidence Packet Template
|
||||
|
||||
Create the packet outside the repository if it contains identities, secrets, raw
|
||||
transcripts, or signed forms. Store only redacted summaries or paths here.
|
||||
|
||||
```markdown
|
||||
# Hanshin Data Governance Evidence Packet
|
||||
|
||||
Packet ID:
|
||||
Evidence root:
|
||||
Prepared by:
|
||||
Data steward:
|
||||
Legal/privacy reviewer:
|
||||
Auth owner:
|
||||
Review date:
|
||||
|
||||
## Decisions
|
||||
- SSO protocol:
|
||||
- User identifier claim:
|
||||
- Role source:
|
||||
- Allowed email domains:
|
||||
- Source data allowed:
|
||||
- Minor/source-case data decision:
|
||||
- Third-party model transfer decision:
|
||||
- Retention and withdrawal decision:
|
||||
|
||||
## Attached Evidence
|
||||
- Official inquiry:
|
||||
- Hanshin response:
|
||||
- IdP metadata/discovery:
|
||||
- Redacted claim samples:
|
||||
- Consent forms reviewed:
|
||||
- Privacy review:
|
||||
- Dataset inventory:
|
||||
- Withdrawal runbook:
|
||||
|
||||
## Open Issues
|
||||
-
|
||||
|
||||
## Decision
|
||||
- [ ] Approved for technical dry run only.
|
||||
- [ ] Approved for live pilot with listed limits.
|
||||
- [ ] Blocked pending external evidence.
|
||||
```
|
||||
|
||||
## Exit Criteria
|
||||
|
||||
This gate is closed only when:
|
||||
|
||||
- Every required artifact owner and reviewer is named.
|
||||
- Evidence paths exist for the official inquiry, written response, consent matrix,
|
||||
SSO claim map, and privacy review.
|
||||
- Live gaps above are either closed with evidence or explicitly converted into
|
||||
launch blockers.
|
||||
- No raw identities, secrets, signed consent forms, raw transcripts, cookies, or
|
||||
tokens are committed to the repository.
|
||||
115
docs/ops/layout-redesign-handoff-2026-06-26.md
Normal file
115
docs/ops/layout-redesign-handoff-2026-06-26.md
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
# Layout Redesign Handoff
|
||||
|
||||
Updated: 2026-06-26 18:30 KST
|
||||
|
||||
## Intent
|
||||
|
||||
The user clarified that this was not meant to be a narrow overflow audit. The layout pass was allowed to substantially redesign screens because the prior UI had protruding elements, wasteful sizing, empty gaps, weak visual quality, and layouts that were not useful enough for repeated work.
|
||||
|
||||
The user also explicitly asked the main thread not to implement the layout redesign directly. The main thread coordinated subagents, reviewed results, ran integration verification, and recorded this handoff.
|
||||
|
||||
## Subagent Work Completed
|
||||
|
||||
### First pass: containment and clipping fixes
|
||||
|
||||
- Session layout: removed the tablet fixed right-panel overlap risk and added intersection checks.
|
||||
- `apps/web/src/pages/session/session.css`
|
||||
- `apps/web/e2e/session-layout.spec.ts`
|
||||
- Admin tablet layout: moved create-user containment behavior up to tablet widths and added 861/900/1024 checks.
|
||||
- `apps/web/src/pages/Admin.tsx`
|
||||
- `apps/web/e2e/admin.spec.ts`
|
||||
- Settings engine controls: made segmented controls shrink/wrap safely and added local clipping assertions.
|
||||
- `apps/web/src/pages/settings/settings.css`
|
||||
- `apps/web/e2e/settings.spec.ts`
|
||||
- Professor recent sessions: replaced mobile horizontal table dependence with responsive readable rows.
|
||||
- `apps/web/src/pages/Professor.tsx`
|
||||
- `apps/web/e2e/teacher.spec.ts`
|
||||
|
||||
### Second pass: actual layout redesign
|
||||
|
||||
- Session: rebalanced the active practice screen around transcript/input, compact client status, context-first desktop rail, and tablet/mobile case summary.
|
||||
- `apps/web/src/pages/Session.tsx`
|
||||
- `apps/web/src/pages/session/session.css`
|
||||
- `apps/web/e2e/session-layout.spec.ts`
|
||||
- Learner home and review: redesigned learner home as a denser practice dashboard and rebuilt review into explicit overview, feedback, evidence, coaching, and transcript regions.
|
||||
- `apps/web/src/pages/LearnerHome.tsx`
|
||||
- `apps/web/src/pages/SessionReview.tsx`
|
||||
- `apps/web/src/pages/session-review/session-review.css`
|
||||
- `apps/web/e2e/learner.spec.ts`
|
||||
- `apps/web/e2e/session-review.spec.ts`
|
||||
- Admin and Professor: redesigned operational screens with compact status strips, sidecar create-user form, dense service/user rows, bounded panels, and responsive recent-session/review areas.
|
||||
- `apps/web/src/pages/Admin.tsx`
|
||||
- `apps/web/src/pages/Professor.tsx`
|
||||
- `apps/web/e2e/admin.spec.ts`
|
||||
- `apps/web/e2e/teacher.spec.ts`
|
||||
- Settings: rebuilt settings as a sticky left rail plus dense main control grid, tightened panels and save footers, and kept mobile controls clipping-safe.
|
||||
- `apps/web/src/pages/Settings.tsx`
|
||||
- `apps/web/src/pages/settings/settings.css`
|
||||
- `apps/web/e2e/settings.spec.ts`
|
||||
|
||||
## Verification
|
||||
|
||||
Commands run from `D:\workspace\vignette\apps\web`:
|
||||
|
||||
```powershell
|
||||
npm run typecheck
|
||||
npx playwright test e2e/learner.spec.ts e2e/session-layout.spec.ts e2e/session-review.spec.ts e2e/admin.spec.ts e2e/settings.spec.ts e2e/teacher.spec.ts --project=chromium-desktop --project=chromium-mobile
|
||||
```
|
||||
|
||||
Results:
|
||||
|
||||
- `npm run typecheck`: passed.
|
||||
- First combined layout E2E: 53 passed, 1 Admin tablet test failed with a Playwright `response.json` body retrieval protocol error.
|
||||
- Isolated Admin tablet rerun: passed.
|
||||
- Final combined layout E2E rerun: 54 passed.
|
||||
|
||||
Worker-reported focused verification also passed:
|
||||
|
||||
- Session layout: 8 passed.
|
||||
- Learner + review: 14 passed.
|
||||
- Admin + teacher: 20 passed.
|
||||
- Settings: 12 passed.
|
||||
|
||||
## Remaining Review Needed
|
||||
|
||||
- Human visual acceptance is still required. The automated checks prove containment, no horizontal overflow, no core overlap, mobile readability, and major control clipping coverage; they do not prove that the new design is subjectively good enough.
|
||||
- Check actual browser screenshots before calling the layout work fully done. Suggested widths:
|
||||
- `390x844`
|
||||
- `720x900`
|
||||
- `861x900`
|
||||
- `900x900`
|
||||
- `1024x768`
|
||||
- `1280x800`
|
||||
- `1440x900`
|
||||
- Pay special attention to dense real data states: long learner names, long session ids, long persona names, many review items, and narrow mobile with sticky navigation.
|
||||
- This pass did not close external/live project gaps such as public Google OAuth `/turn`, live Postgres RLS DSN proof, physical microphone/public WSS proof, or production SAML XML validation.
|
||||
|
||||
## Next Suggested Step
|
||||
|
||||
Start the next session by visually inspecting the changed pages in a real browser, then decide whether the new layout direction is acceptable or whether another page-specific design pass is needed. Do not mark the dashboard layout item as `DONE` until that human visual review is complete.
|
||||
|
||||
## Visual Acceptance Closure (2026-06-26, follow-up session)
|
||||
|
||||
The visual acceptance requested above was completed and the dashboard layout item was moved to `DONE`.
|
||||
|
||||
What was added:
|
||||
|
||||
- A strict, automated visual gate: `apps/web/e2e/layout-visual-gate.spec.ts`. It renders the 7 redesigned screens (learner home, session prestart, session active, session review, professor, admin, settings) at all 7 suggested widths (390/720/861/900/1024/1280/1440), asserts horizontal overflow 0 and zero clipped controls (hard-clip by a non-scrollable overflow ancestor, or text clipped without an ellipsis/line-clamp affordance), and writes 49 full-page screenshots to `node_modules/.tmp/layout-gate/`. Dense real-data states are seeded (long learner name, 3 stacked sessions, an ended-session review). Horizontal carousels and intentional ellipsis/line-clamp are excluded from the clip check to avoid false positives.
|
||||
|
||||
Process (parallel):
|
||||
|
||||
1. A 7-agent review workflow read all 49 screenshots and reported per-screen findings.
|
||||
2. A 6-agent fix team (one per page, disjoint files) fixed the findings.
|
||||
3. Integrated re-verification: `npm run typecheck` OK, gate 7 passed (re-screenshot), layout focused E2E desktop/mobile 54 passed (no regression).
|
||||
4. A 7-agent adversarial verify workflow confirmed 7/7 `accept`, critical/major resolved, 0 regressions.
|
||||
|
||||
Key defects resolved:
|
||||
|
||||
- Learner home 1280/1440 three-column collapse where the center preview column shrank and persona meta broke one-char-per-line (critical) — the body two-column split was raised from `min-width:1180px` to `1400px` and `word-break:keep-all` added.
|
||||
- Professor raw `API 404: Not Found` banner — 404 now renders the normal empty state; only real failures show a tidy retry card.
|
||||
- Session review empty-state black card hierarchy inversion — strong styling is now conditional on real feedback.
|
||||
- Admin empty/loading gray boxes — shimmer skeletons and placeholder text.
|
||||
- Settings wide-desktop left dead zone — slimmer rail + centered body; engine-mode radios in a 2×2 grid.
|
||||
- Session mobile control bar — 44px touch targets, labeled "밀어서 종료", stronger primary send button.
|
||||
|
||||
Remaining items are cosmetic minor only (recorded as a non-blocking polish backlog in `docs/dev_dashboard.html`), several of which are visible only in the zero-data empty state.
|
||||
29
docs/ops/postgres-rls-audit-smoke.md
Normal file
29
docs/ops/postgres-rls-audit-smoke.md
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# Postgres RLS/Audit Smoke
|
||||
|
||||
Use this only against a local or disposable dev Postgres database initialized from
|
||||
`infra/db/init/*.sql`. The checker writes temporary fixture learners, sessions,
|
||||
turns, and audit rows, then removes only rows tagged with its unique run id.
|
||||
|
||||
## Run
|
||||
|
||||
Use the non-owner app role created by `infra/db/init/99_app_role.sh`; do not use
|
||||
`postgres`, a superuser, a `BYPASSRLS` role, or a table owner.
|
||||
|
||||
```powershell
|
||||
$env:VIGNETTE_RLS_AUDIT_DSN = "postgresql://vignette_app:vignette_app@127.0.0.1:5432/vignette"
|
||||
C:\Users\encep\AppData\Local\Programs\Python\Python311\python.exe scripts\check-postgres-rls-audit.py --write-fixtures
|
||||
```
|
||||
|
||||
The script intentionally ignores `DATABASE_URL`. Without an explicit smoke DSN
|
||||
and `--write-fixtures`, it prints prerequisites and exits nonzero.
|
||||
|
||||
## Checks
|
||||
|
||||
- Learner A can read their own session, but cannot read learner B's session or turns.
|
||||
- Instructor visibility is scoped to `app.current_cohort`.
|
||||
- An evaluator-only turn is hidden from learner/client/counselor AI context and visible to evaluator AI.
|
||||
- Instructor and admin read paths can insert `audit.audit_log` `read_session` rows.
|
||||
|
||||
Any proof that cannot be made from the current schema or connection role is not
|
||||
reported as passed. Default exit code is nonzero for failures or skipped proof;
|
||||
use `--allow-skips` only when intentionally probing a partial local schema.
|
||||
100
docs/ops/public-runtime-watchdog.md
Normal file
100
docs/ops/public-runtime-watchdog.md
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
# Public Runtime Watchdog
|
||||
|
||||
This runbook keeps the public Vignette runtime recoverable after Windows
|
||||
reboot, update, or process crash. It covers:
|
||||
|
||||
- engine gateway: `http://127.0.0.1:9099`
|
||||
- prod API: `http://127.0.0.1:8001`
|
||||
- Cloudflare tunnel for `https://api-vignette.chanpaca.net`
|
||||
|
||||
## Secret Handling
|
||||
|
||||
Do not put secrets in scheduled task arguments.
|
||||
|
||||
The scripts use the existing runtime locations:
|
||||
|
||||
- API secrets stay in `apps/api/.env`.
|
||||
- Cloudflared credentials stay under the current user's `.cloudflared` config.
|
||||
- Claude CLI OAuth/config stays in the current Windows user profile.
|
||||
|
||||
The installer creates a per-user interactive scheduled task. It starts at user
|
||||
logon and repeats as a watchdog. Fully unattended boot before any user logs in
|
||||
requires an operator-managed service account or Task Scheduler credential; do
|
||||
that in Windows, not by adding secrets to these scripts.
|
||||
|
||||
## Install Or Update
|
||||
|
||||
From the repo root:
|
||||
|
||||
```powershell
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\install-public-runtime-task.ps1 -RunNow
|
||||
```
|
||||
|
||||
The installer is idempotent. Re-running it updates the same task:
|
||||
|
||||
```powershell
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\install-public-runtime-task.ps1 -IntervalMinutes 5
|
||||
```
|
||||
|
||||
Task name:
|
||||
|
||||
```powershell
|
||||
VignettePublicRuntimeWatchdog
|
||||
```
|
||||
|
||||
## Manual Start
|
||||
|
||||
Use this when you want to force a runtime restore immediately:
|
||||
|
||||
```powershell
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\start-public-runtime.ps1
|
||||
```
|
||||
|
||||
`start-public-runtime.ps1` now verifies or starts the engine gateway before it
|
||||
starts the prod API and cloudflared.
|
||||
|
||||
## Health Checks
|
||||
|
||||
```powershell
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\watch-public-runtime.ps1 -CheckOnly
|
||||
Invoke-RestMethod http://127.0.0.1:9099/health
|
||||
Invoke-RestMethod http://127.0.0.1:8001/health
|
||||
Invoke-RestMethod https://api-vignette.chanpaca.net/health
|
||||
```
|
||||
|
||||
Optional real Claude CLI readiness smoke:
|
||||
|
||||
```powershell
|
||||
Invoke-RestMethod http://127.0.0.1:9099/ready
|
||||
```
|
||||
|
||||
`/ready` can consume a small Claude budget because it performs a real generation.
|
||||
|
||||
## Logs And Task State
|
||||
|
||||
```powershell
|
||||
Get-ScheduledTask -TaskName VignettePublicRuntimeWatchdog
|
||||
Get-ScheduledTaskInfo -TaskName VignettePublicRuntimeWatchdog
|
||||
Get-Content .\public-runtime-watchdog.log -Tail 50
|
||||
Get-Content .\apps\api\engine.public.err.log -Tail 50
|
||||
Get-Content .\apps\api\api.public.err.log -Tail 50
|
||||
Get-Content .\cloudflared.public.err.log -Tail 50
|
||||
```
|
||||
|
||||
## Remove
|
||||
|
||||
```powershell
|
||||
Unregister-ScheduledTask -TaskName VignettePublicRuntimeWatchdog -Confirm:$false
|
||||
```
|
||||
|
||||
## Recovery Notes
|
||||
|
||||
If local health is good but public health fails, inspect the cloudflared process
|
||||
and `C:\Users\<user>\.cloudflared\vignette-config.yml`.
|
||||
|
||||
If engine health fails, verify that `claude` runs for the same Windows user that
|
||||
owns the scheduled task and that the user has completed Claude CLI login.
|
||||
|
||||
If API health fails with `environment`, `db`, or auth configuration errors,
|
||||
inspect `apps/api/.env`; do not copy secret values into scripts or task
|
||||
arguments.
|
||||
625
docs/ops/refactor-rag-patches-2026-06-26.json
Normal file
625
docs/ops/refactor-rag-patches-2026-06-26.json
Normal file
File diff suppressed because one or more lines are too long
272
docs/ops/refactor-rag-patches-2026-06-26.md
Normal file
272
docs/ops/refactor-rag-patches-2026-06-26.md
Normal file
|
|
@ -0,0 +1,272 @@
|
|||
# 리팩토링 + RAG 활성화 패치 (워크플로우 산출, 2026-06-26)
|
||||
|
||||
> 적용 순서·리스크는 아래 'Sequence' 참고. 일부 anchor는 이후 voice.py 수정(speakable_text)으로 stale일 수 있어 적용 전 현재 코드와 대조 필수.
|
||||
|
||||
## 적용 순서/리스크 (Sequence)
|
||||
|
||||
두 핵심 충돌을 코드로 확정했습니다. orchestrator.py `__all__`(399–401: `EvalHook`/`LogHook`)과 sessions.py `prepare_turn`(903·979, 인자 `recall_summary=`/`recent_turns=`)이 실제 겹칩니다. 또한 현재 `prepare_turn` 시그니처(97–107)에 `kb_behavior_cues`가 **이미 분리 키워드로 존재**함을 확인했습니다 — 즉 P4(RAG)는 이 구버전 시그니처를 전제로 작성되어 P2와 정면 충돌합니다.
|
||||
|
||||
---
|
||||
|
||||
# 리팩토링 4종 적용 순서·의존·리스크 분석
|
||||
|
||||
분석 대상 4패치:
|
||||
|
||||
| 코드 | 패치 | 성격 | risk |
|
||||
|---|---|---|---|
|
||||
| **P1** | 데드코드 3종 제거 (LogHook / gateway·client tier / 서버 RMS 힌트) | 순수 삭제 | medium |
|
||||
| **P2** | 파라미터 객체화 3종 (`OpennessParams` / `TurnMemory` / `ManagedUserInput`) | 시그니처 리팩토링 | medium |
|
||||
| **P3** | `turn_runtime.py` 추출 (REST/WS 턴 처리 공용화) | 코드 이동·래퍼 | medium |
|
||||
| **P4** | `routes/sessions.py` RAG 배선 (회상·KB 행동단서) | 기능 추가 | medium |
|
||||
|
||||
---
|
||||
|
||||
## 1. 파일 겹침 매트릭스
|
||||
|
||||
| 파일 | P1 | P2 | P3 | P4 |
|
||||
|---|:-:|:-:|:-:|:-:|
|
||||
| `services/orchestrator.py` | ● | ● | | |
|
||||
| `routes/sessions.py` | | ● | ● | ● |
|
||||
| `routes/voice.py` | ● | ● | ● | |
|
||||
| `services/voice.py` | ● | | | |
|
||||
| `services/evaluator.py` | ● | ● | | |
|
||||
| `services/persona.py` / `state_machine.py` | | ● | | |
|
||||
| `auth_sessions.py` / `routes/admin.py` | | ● | | |
|
||||
| `engine_client.py` / `engine_gateway/gateway.py` | ● | | | |
|
||||
| `routes/eval.py` | | ● | | |
|
||||
| `turn_runtime.py` (신규) | | | ● | |
|
||||
| `test_session_turn_persistence.py` | ● | ● | | |
|
||||
| `test_orchestrator_masking.py` / `test_rbac_idor.py` | (●) | ● | | |
|
||||
|
||||
가장 뜨거운 파일: **`routes/sessions.py`(P2·P3·P4)**, **`routes/voice.py`(P1·P2·P3)**, **`orchestrator.py`(P1·P2)**.
|
||||
|
||||
---
|
||||
|
||||
## 2. 충돌·의존 분석 (앵커·시그니처 기준)
|
||||
|
||||
### C1. P1 ↔ P2 — orchestrator `__all__` (대칭 앵커 충돌, 경미)
|
||||
- 두 패치 모두 `__all__`(399–401)의 `EvalHook`/`LogHook` 3행 창을 건드린다. P1은 `LogHook` 삭제, P2는 `LogHook` 뒤에 `TurnMemory` 삽입.
|
||||
- **둘 다 동일 HEAD 기준 병렬 작성**이라, 나중에 적용되는 쪽 1줄을 리베이스해야 한다.
|
||||
- 그 외에는 거의 직교: P1은 `LogHook 타입/run_turn_generate·run_turn_stream 훅/GenerateRequest·StreamRequest tier`(엔진요청·실행함수), P2는 `TurnContext 필드/prepare_turn 본문`(별개 함수). **같은 함수 안에서 충돌하는 줄은 없음.**
|
||||
|
||||
### C2. P2 ↔ P4 — sessions.py `prepare_turn` (구조적 충돌, 치명)
|
||||
- 현 `prepare_turn`은 `recall_summary/pinned_facts/recent_turns/kb_behavior_cues` **분리 키워드**(97–107). P4는 이 시그니처에 맞춰 호출부(903·979)에 `kb_behavior_cues=kb_cues`를 **추가**하도록 작성됨.
|
||||
- **P2는 이 4개 키워드를 단일 `memory: TurnMemory`로 교체**한다. 따라서 P2 이후엔:
|
||||
- P4의 `prepare_turn(..., kb_behavior_cues=kb_cues)` 호출은 **TypeError**(해당 키워드 소멸).
|
||||
- P4의 앵커(`recall_summary=...\nrecent_turns=...`)도 매칭 실패(이미 `memory=...`로 치환됨).
|
||||
- **결론: P4는 반드시 P2 뒤에 오고, 호출부를 다음으로 재작성해야 한다.**
|
||||
```python
|
||||
memory=orchestrator.TurnMemory(
|
||||
recall_summary=recall.recall_summary,
|
||||
pinned_facts=recall.pinned_facts,
|
||||
recent_turns=sess.recent_turns(visible_to="client"),
|
||||
kb_behavior_cues=kb_cues,
|
||||
)
|
||||
```
|
||||
- 역순(P4 먼저)도 불가: P4가 `kb_behavior_cues` 줄을 끼워 넣으면 P2 앵커가 깨지고, P2의 sessions.py `TurnMemory(...)` 치환문엔 `kb_behavior_cues`가 없어 **RAG 배선이 통째로 누락**된다.
|
||||
|
||||
### C3. P2·P3·P4 — sessions.py 동일 파일, **분리 영역**(병존 가능)
|
||||
- P2 = `init_state`(712)·`prepare_turn`(903·979)
|
||||
- P3 = `_load_session_or_404`(221)·`_append_session_turn`(241)·`_update_session_state`(254)·완료턴 영속화 블록(928·941·956·1002·1012·1014)
|
||||
- P4 = 상단 캐시/헬퍼 신설·세션 시작 회상 재조립·`prepare_turn` 호출부·종료 캐시 정리
|
||||
- P2와 P3는 **서로 다른 함수/줄** → 순서 무관(commutative). P4만 P2의 `prepare_turn`과 충돌(C2).
|
||||
|
||||
### C4. P1 ↔ P3 / P2 ↔ P3 — voice.py, **분리 영역**
|
||||
- P1 = `synthesize_stream` 송신 루프(tts_chunk 제거), P2 = `init_state`/`prepare_turn`, P3 = 완료턴 영속화 블록·`_load_voice_session`·`_append_voice_turn`/`_update_voice_state` 제거. 셋 다 다른 영역 → 병존 가능.
|
||||
|
||||
### 하드 제약 요약
|
||||
1. **P4는 P2 이후**(+호출부 재작성). ← 유일한 강제 순서.
|
||||
2. P1·P2 중 나중 적용분은 orchestrator `__all__` 1줄 리베이스.
|
||||
3. P3는 P1·P2와 직교(영역 분리), P2와 commutative.
|
||||
|
||||
---
|
||||
|
||||
## 3. 권장 적용 순서: **P1 → P2 → P3 → P4**
|
||||
|
||||
충돌 최소·안전 우선 근거:
|
||||
|
||||
1. **P1 먼저** — 순수 삭제라 가장 안전하고 코드 표면을 줄인다. 독립 파일(`engine_client.py`/`gateway.py`/`services/voice.py`)을 먼저 비워 이후 패치의 변경면을 좁힌다. P2와의 유일 접점은 `__all__` 1줄.
|
||||
2. **P2 다음** — 시그니처 리팩토링은 호출부 표류 위험이 크므로, 기능 추가(P4) 전에 **인자 형태를 먼저 확정**한다. P4의 선행 의존이라 반드시 P4보다 앞.
|
||||
- 적용 시 orchestrator `__all__`은 P1이 `LogHook`을 이미 지웠으므로, P2의 삽입 앵커를 `"EvalHook",\n "TurnContext",` → `"EvalHook",\n "TurnMemory",\n "TurnContext",`로 조정(원안의 `LogHook` 포함 앵커/치환문에서 `LogHook` 제거).
|
||||
3. **P3** — P2와 영역이 분리되어 순서 자유지만, `prepare_turn` 주변이 P2로 안정된 뒤 영속화/로드 골격을 옮기는 편이 리뷰가 명확. (P2↔P3는 교환 가능 — 일정상 병렬 작업도 가능.)
|
||||
4. **P4 마지막** — P2의 `memory=TurnMemory` 신시그니처에 맞춰 **재작성한 버전**으로 적용. RAG는 graceful degradation이라 기능적으로 마지막에 얹어도 회귀 표면이 가장 작다.
|
||||
|
||||
> P4를 원안(JSON) 그대로 적용하면 **반드시 깨진다**. 이 분석의 단일 최대 리스크이며, P4 호출부 2곳(submit_turn·stream_turn)을 `TurnMemory(... kb_behavior_cues=kb_cues)`로 바꾸는 재작성이 선행되어야 한다.
|
||||
|
||||
---
|
||||
|
||||
## 4. 단계별 테스트 (작업 디렉터리 `apps/api`)
|
||||
|
||||
### P1 후
|
||||
```
|
||||
python -m pytest app/ engine_gateway/ -q
|
||||
cd ../web && npx playwright test e2e/voice-success.spec.ts
|
||||
```
|
||||
- 중점: `test_voice_service`(synthesize_stream/fallback), `test_session_turn_persistence`(TTSChunk), `test_orchestrator_masking`(tier 키 제거 무영향), `test_gateway_model`, `test_rbac_idor`.
|
||||
- 사후 grep(잔존 0 확인): `log_hook`·`LogHook`·`estimate_chunk_rms`·`ck.rms`/`ck.seq`/`tts_chunk`·`GenerateRequest|StreamRequest|GwGenerateReq`의 `tier`. voice.py `math` 미참조, orchestrator `Awaitable`/`Callable`는 `EvalHook` 경유 잔존 확인.
|
||||
|
||||
### P2 후
|
||||
```
|
||||
python -c "from app.services import persona, orchestrator, state_machine; from app import auth_sessions; from app.routes import admin; from app.routes import eval as _e"
|
||||
python -m pytest app/test_state_machine_resistance.py app/test_orchestrator_masking.py app/test_session_turn_persistence.py app/test_rbac_idor.py -q
|
||||
python -m pytest app -q && python -m pytest engine_gateway -q
|
||||
```
|
||||
- 중점: 순환참조 부재(`persona→state_machine` 단방향, `orchestrator→persona(TurnMemory)`, `eval→orchestrator(TurnMemory)`), PII 마스킹 불변(`memory=TurnMemory`로 받아도 recall/pinned/recent 마스킹 유지), 저항 곡선 불변, `ctx.memory.recent_turns`가 evaluator-only 비공개 발화 제외, auth/admin 경로(`ManagedUserInput`) 회귀.
|
||||
- 잔존 참조 점검: `ctx.recall_summary`/`ctx.pinned_facts`/`ctx.recent_turns`/`ctx.kb_behavior_cues` 가 evaluator.py·eval.py·테스트에 남아있지 않은지(AttributeError 차단).
|
||||
|
||||
### P3 후
|
||||
```
|
||||
python -c "import app.turn_runtime, app.routes.sessions, app.routes.voice"
|
||||
python -m pytest app/test_session_turn_persistence.py app/test_rbac_idor.py app/test_voice_ws.py -q
|
||||
python -m pytest app/ engine_gateway/ -q
|
||||
```
|
||||
- 중점: generate/stream 텔레메트리 영속(2턴, client provider/tokens/cost), 엔진 실패 시 `sess.turns==[]`(학습자 전용 발화 미생성), voice 오디오 메타(audio_ref/silence_ms/speech_rate/barge_in), IDOR 403 `does not belong`, WS 컨트랙트(ready/state/pong) 순서·`unknown session {id}` 문구, `turn_runtime`이 routes 미import(순환 없음). `test_runtime_policy` 무영향 확인.
|
||||
- 주의: `test_rbac_idor`의 `patch.object(sessions,'runtime_fallback_allowed')`는 로드 로직 이동 후 no-op이지만 dev 기본값(True)으로 통과 유지 — 엄밀화하려면 patch 대상을 `turn_runtime.runtime_fallback_allowed`로 바꾸는 선택적 후속.
|
||||
|
||||
### P4 후 (재작성본 기준)
|
||||
```
|
||||
python -c "import app.main"
|
||||
python -m pytest app -q && python -m pytest engine_gateway -q
|
||||
```
|
||||
- 중점: DB 풀 미초기화 상태에서 `_retrieve_kb_behavior_cues(P1)`·`_build_start_recall(...)`이 예외 없이 `[]`·빈 `RecallContext` 반환(get_pool RuntimeError 경로, graceful). KB 인덱싱+임베더 환경에서 `ctx.messages` L2에 행동단서 블록 포함, 미설치 환경에선 빈 cues로 200 유지. P2 신시그니처 정합(`prepare_turn`이 `memory=` 만 받는지) 재확인.
|
||||
|
||||
---
|
||||
|
||||
## 5. 리스크 종합
|
||||
|
||||
- **최대 리스크 — P4 ↔ P2 시그니처 불일치(C2):** P4 원안은 구버전 `prepare_turn`(분리 키워드)에 묶여 있어 P2 적용 환경에서 **TypeError + 앵커 미스 + RAG 배선 누락**을 유발. P4는 반드시 P2 뒤에서 `memory=TurnMemory(... kb_behavior_cues=kb_cues)`로 재작성. 이것이 순서를 강제하는 유일·결정적 의존이다.
|
||||
- **경미 리스크 — orchestrator `__all__`(C1):** P1·P2 병렬 작성으로 동일 3행 창 충돌. 나중 적용분 1줄 리베이스로 해소.
|
||||
- **분산 리스크 — sessions.py/voice.py 다중 패치(C3·C4):** 영역이 분리되어 병존 가능하나, 한 파일에 3패치가 누적되므로 각 단계 후 `import` sanity + 해당 라우트 회귀를 반드시 통과시킨 뒤 다음 단계로.
|
||||
- **공통 완화책:** 각 패치가 자체 테스트 편집을 포함하므로 단계별 green을 게이트로 사용. 단계 사이 grep 기반 잔존 식별자 0 검증으로 "부분 적용" 누수를 차단.
|
||||
|
||||
**한 줄 요약:** `P1(삭제) → P2(시그니처 확정) → P3(추출, P2와 교환 가능) → P4(RAG, P2 신시그니처로 재작성 필수)`. 강제 제약은 **P4-after-P2**, 나머지는 영역 분리로 병존 가능하며, orchestrator `__all__` 1줄과 sessions.py `prepare_turn` 호출부 재작성이 손대야 할 두 접합부다.
|
||||
|
||||
## 리팩토링 패치
|
||||
|
||||
|
||||
### 데드코드 3종 제거: (1) orchestrator.py LogHook 타입·log_hook 파라미터·분기·export, (2) engine_gateway/gateway.py tier dead-contract(클라이언트 측 tier 동반 제거), (3) services/voice.py·routes/voice.py 서버 RMS 힌트(tts_chunk.rms/seq + estimate_chunk_rms). (risk=medium)
|
||||
|
||||
- 근거: 세 제거 모두 실제 grep으로 참조 0건을 입증했다. (1) LogHook/log_hook: 본 식별자는 orchestrator.py 안에서만 등장하고, run_turn_generate/run_turn_stream의 모든 호출부(sessions.py:916/995, voice.py:307, 테스트들)는 eval_hook 또는 위치인자만 전달하므로 주입 caller가 0 — 순수 데드. run_turn_stream은 키워드 전용 인자가 사라지므로 `*,` 마커도 함께 제거해야 SyntaxError를 막는다(위치 호출부라 영향 없음). (2) gateway tier: 게이트웨이는 req.tier를 어디서도 읽지 않고 모델 선택은 model override→세션모델→DEFAULT_MODEL뿐이다. 게이트웨이가 단일모델 상주 claude -p 엔진(docstring)이라 tier→model 배선은 설계상 귀속처가 없으므로 '배선'이 아니라 '제거'가 옳다. 클라이언트 측 tier는 값이 어떤 동작에도 영향하지 않는 write-only 데드여서 동반 제거하되, 어떤 테스트도 tier를 참조하지 않아 회귀 위험이 낮다(pydantic 기본 extra=ignore라 부분 잔존 시에도 422 없음). (3) 서버 RMS 힌트: 프론트 Session.tsx onmessage에 tts_chunk 분기가 없어 메시지가 통째로 무시되고, 립싱크 진폭은 useAvatarMotion.ts가 Web Audio로 자체 산출한다(voice.py 주석도 이를 명시). 따라서 rms/seq + estimate_chunk_rms(불필요한 바이트 루프 비용)는 dead이며 제거로 핫패스 JSON 직렬화/전송과 CPU를 절감한다. risk=medium은 (3)이 WebSocket 프로토콜과 e2e/단위 어서션을 건드리기 때문이며, 해당 테스트 편집을 패치에 포함해 회귀를 차단했다.
|
||||
- 검증: 1) 백엔드 단위테스트(레포 루트): `cd apps/api && python -m pytest app/ engine_gateway/ -q` (또는 `python -m unittest discover -s apps/api`). 중점 파일: test_orchestrator_masking.py(마스킹 payload — tier 키 제거가 어서션에 영향 없음), test_session_turn_persistence.py(TTSChunk·run_turn_stream 텔레메트리), test_voice_service.py(synthesize_stream/fallback), test_gateway_model.py(GwGenerateReq는 tier 미사용 — 통과), test_rbac_idor.py(run_turn_generate 패치). 위 test 편집 3건 반영 시 전부 green 기대.\n2) 프론트 e2e: `cd apps/web && npx playwright test e2e/voice-success.spec.ts` — tts_chunk 기대 제거 후 통과, binaryChunks>0 유지 확인. mic UI 시나리오(@single-run)도 회귀 없음 확인.\n3) 사후 grep(read-only)로 잔존 0 확인: `log_hook`, `LogHook`, `estimate_chunk_rms`, voice의 `ck.rms`/`ck.seq`/`tts_chunk`, GenerateRequest/StreamRequest/GwGenerateReq의 `tier`. 모두 0건이어야 함.\n4) import 정합: voice.py에서 `math` 미참조 확인, orchestrator.py에서 `Awaitable`/`Callable`는 EvalHook가 계속 사용함 확인.
|
||||
- affected_callers: ['apps/api/app/services/orchestrator.py:197 (GenerateRequest tier 인자 제거 — 본 패치 포함)', 'apps/api/app/services/orchestrator.py:272 (StreamRequest tier 인자 제거 — 본 패치 포함)', 'apps/api/app/services/evaluator.py:637 (fast GenerateRequest tier 제거 — 본 패치 포함)', 'apps/api/app/services/evaluator.py:694 (deep GenerateRequest tier 제거 — 본 패치 포함)', 'apps/api/app/routes/voice.py:383 (ck.rms/ck.seq 사용처 — tts_chunk 송신 제거로 함께 정리)', 'LogHook 시그니처 변경 호출부(갱신 불필요, 확인만): apps/api/app/routes/sessions.py:916, apps/api/app/routes/sessions.py:995, apps/api/app/routes/voice.py:307']
|
||||
|
||||
- edits: 28건
|
||||
|
||||
|
||||
### 파라미터 객체화 3종 리팩토링 (모두 읽기전용 분석 → 적용 가능한 정확 패치 스펙):
|
||||
(1) state_machine.init_state(base_resistance/unlock_rate/decay_floor/ideation_baseline) → frozen dataclass OpennessParams 단일 인자 + PersonaCard.openness_params() 팩토리.
|
||||
(2) orchestrator.prepare_turn / persona.build_turn_messages 의 recall_summary/pinned_facts/recent_turns/kb_behavior_cues → 공유 TurnMemory dataclass (persona.py 정의, orchestrator 재노출). TurnContext 도 memory:TurnMemory 단일 필드로.
|
||||
(3) auth_sessions.upsert_managed_user / _memory_upsert_managed_user 의 7키워드(email/display_name/role/cohort_ids/user_id/affiliation/reactivate) → pydantic ManagedUserInput. (risk=medium)
|
||||
|
||||
- 근거: 세 리팩토링 모두 "동작 보존 + 인자 묶음"으로 부수효과가 없다. (1) OpennessParams 는 init_state 의 4개 키워드를 frozen dataclass 로 그대로 옮기며 계산식(compute_effective_openness/carry-over 로직)은 한 글자도 바꾸지 않았다. PersonaCard.openness_params() 는 기존 base_resistance()/unlock_rate()/decay_floor()/ideation_baseline() 접근자를 그대로 호출하는 얇은 팩토리라 값이 동일하다. persona→state_machine 은 단방향 import(state_machine 은 어떤 앱 모듈도 import 하지 않음)라 순환참조가 없다. (2) TurnMemory 는 prepare_turn 입력 4종 + TurnContext 저장 + build_turn_messages 입력을 하나로 통일해 "마스킹 1회 → 동일 객체 공유" 흐름을 명확히 한다. 마스킹은 prepare_turn 본문에서 그대로 수행되므로 PII 불변식이 유지된다. TurnContext 필드 축소로 ctx.recent_turns 를 읽던 evaluator.py·eval.py·test_rbac_idor.py 3곳을 ctx.memory.recent_turns 로 동시 갱신해 누락이 없다. (3) ManagedUserInput(pydantic) 는 upsert/_memory_upsert 가 공유하던 동일한 7키워드 중복을 제거하고, fallback 경로는 `_memory_upsert_managed_user(inp)` 로 단순화돼 email 재정규화가 함수 내부에서 멱등 처리된다. 모든 호출부(create_session, admin.create_user, 내부 3곳)를 ManagedUserInput 생성으로 교체했고 외부 엔드포인트/공개 함수(create_session) 시그니처는 불변이라 라우트 회귀가 없다. 가치: 4파라미터 init_state 5호출부, 4파라미터 prepare_turn 7호출부, 7키워드 upsert 5호출부의 시그니처 표류 위험을 타입 객체 하나로 수렴시켜 향후 파라미터 추가 시 호출부 일괄 누락 버그를 구조적으로 차단한다. medium 위험인 이유는 프로덕션 라우트(sessions/voice/admin)·평가기·인증 경로를 동시에 건드리지만, 모두 기계적 치환이고 단위테스트 77개가 경로를 커버한다.
|
||||
- 검증: 작업 디렉터리 D:\workspace\vignette\apps\api 기준.
|
||||
1) 임포트/순환참조 sanity: `python -c "from app.services import persona, orchestrator, state_machine; from app import auth_sessions; from app.routes import admin; from app.routes import eval as _e"` (persona→state_machine 단방향 import, orchestrator→persona(TurnMemory), eval→orchestrator(TurnMemory) 가 깨지지 않는지 확인).
|
||||
2) 핵심 회귀 4종: `python -m pytest app/test_state_machine_resistance.py app/test_orchestrator_masking.py app/test_session_turn_persistence.py app/test_rbac_idor.py -q` — 특히 (a) test_orchestrator_masking 의 PII 마스킹(prepare_turn 이 memory=TurnMemory 로 받아도 recall/pinned/recent 가 여전히 마스킹되는지), (b) test_state_machine_resistance 의 공감/조언 곡선 불변, (c) test_rbac_idor 의 ctx.memory.recent_turns 가 evaluator-only 비공개 발화를 제외하는지.
|
||||
3) 전체 백엔드 단위테스트 77개: `python -m pytest app -q` (auth/admin/login 경로 — create_session→upsert_managed_user(ManagedUserInput), admin create_user 엔드포인트 포함 회귀 확인).
|
||||
4) 게이트웨이 영향 없음 확인: `python -m pytest engine_gateway -q` (7개).
|
||||
모두 green 이어야 하며, 시그니처 변경분(init_state 단일 인자, prepare_turn/build_turn_messages memory 인자, upsert/_memory_upsert 의 ManagedUserInput)이 호출부와 정합하는지 import 에러/AttributeError(ctx.recall_summary 등 잔존 참조) 부재로 검증.
|
||||
- affected_callers: ['init_state — apps/api/app/session_persistence.py:101', 'init_state — apps/api/app/routes/sessions.py:712', 'init_state — apps/api/app/routes/voice.py:498', 'init_state — apps/api/app/test_orchestrator_masking.py:32', 'init_state — apps/api/app/test_state_machine_resistance.py:29', 'prepare_turn — apps/api/app/routes/sessions.py:903', 'prepare_turn — apps/api/app/routes/sessions.py:979', 'prepare_turn — apps/api/app/routes/voice.py:293', 'prepare_turn — apps/api/app/test_orchestrator_masking.py:41', 'prepare_turn — apps/api/app/test_orchestrator_masking.py:132', 'prepare_turn — apps/api/app/test_session_turn_persistence.py:178', 'prepare_turn — apps/api/app/test_session_turn_persistence.py:213', 'build_turn_messages — apps/api/app/services/orchestrator.py:146 (유일 호출부)', 'TurnContext.memory(생성) — apps/api/app/services/orchestrator.py:115', 'TurnContext.memory(생성) — apps/api/app/routes/eval.py:176 (+ import 171)', 'TurnContext.memory(읽기 ctx.recent_turns) — apps/api/app/services/evaluator.py:381', 'TurnContext.memory(읽기 ctx.recent_turns) — apps/api/app/test_rbac_idor.py:285', 'upsert_managed_user — apps/api/app/auth_sessions.py:675 (create_session)', 'upsert_managed_user — apps/api/app/routes/admin.py:493 (+ import 12-18)', '_memory_upsert_managed_user — apps/api/app/auth_sessions.py:452 (upsert DB 성공)', '_memory_upsert_managed_user — apps/api/app/auth_sessions.py:466 (upsert fallback)', '_memory_upsert_managed_user — apps/api/app/auth_sessions.py:548 (update_managed_user)', 'PersonaCard.openness_params 신규 — apps/api/app/services/persona.py (P1/P2/P3 시드는 변경 불필요)']
|
||||
|
||||
- edits: 44건
|
||||
|
||||
|
||||
### REST(routes/sessions.py submit/stream)와 WS(routes/voice.py)의 턴 처리 중복 제거. 새 공용 모듈 app/turn_runtime.py 추출: stage_label(라벨 일원화) + load_owned_session(로드/오너십/종료 검증 코어) + append_completed_turn/update_session_state(영속화 폴백) + record_completed_turn(상담자 발화+내담자 응답 append+상태 갱신). 각 라우트는 자기 표현(HTTP 예외 / WS 에러문구)으로 매핑하는 얇은 래퍼만 유지. (risk=medium)
|
||||
|
||||
- 근거: REST(sessions submit/stream)와 WS(voice)가 '세션 로드->오너십->완료 턴 영속화->상태 갱신' 절차와 폴백 로직, stage 라벨 변환을 각각 복제하고 있었다(append/update 헬퍼 4종, 로드 헬퍼 2종이 거의 동일). 이를 app/turn_runtime.py 공용 모듈로 추출하면 영속/폴백 정책 변경이 한곳에서 끝난다. 안전성 근거: (1) Stage(str, Enum) 의 .value 가 이미 한글이라 stage_label 과 .value 는 byte-identical -> stage 일원화는 순수 리팩토링(동작 무변). (2) 각 라우트의 외부 계약(HTTP 404/403/409 와 detail 문구, WS 'unknown session {id}' 등)은 얇은 매핑 래퍼로 그대로 보존. (3) record_completed_turn 은 'client_reply 있을 때만 내담자 턴 기록' 규칙과 counselor->client 순서, 폴백 context 문자열까지 동일 재현하여 실패 턴이 학습자 전용 발화를 남기지 않는 기존 보호를 유지. (4) 라우트는 여전히 orchestrator.run_turn_generate/run_turn_stream 을 직접 호출하므로 테스트의 sessions.orchestrator/voice_routes.orchestrator patch 가 그대로 적용된다. (5) turn_runtime 은 routes 를 import 하지 않아 순환 없음. 참고: routes/eval.py 에도 별도의 _load_session_or_404 사본이 있으나(다른 책임) 이번 스코프 밖으로 두어 변경면을 한정했다 — 후속 통합 후보.
|
||||
- 검증: 1) 회귀 핵심: cd apps/api && python -m pytest app/test_session_turn_persistence.py app/test_rbac_idor.py app/test_voice_ws.py -q (현재 baseline 20 passed 확인됨). 검증 포인트 — generate/stream 텔레메트리 영속(2턴, client llm_provider/tokens/cost), 엔진 실패 시 sess.turns==[](학습자 전용 발화 미생성), voice 오디오 메타(audio_ref/silence_ms/speech_rate/barge_in) 부착, IDOR 403 'does not belong', WS 컨트랙트(ready/state/pong) 순서. 2) 전체 백엔드: python -m pytest app/ engine_gateway/ -q (단위 84개 무회귀). 3) 정적: python -c "import app.turn_runtime, app.routes.sessions, app.routes.voice" 로 import/순환 점검. 4) 폴백 정책: test_runtime_policy.py 무영향 확인. 주: test_rbac_idor 의 patch.object(sessions,'runtime_fallback_allowed',True) 는 로드 로직 이동 후 no-op 가 되지만 환경 기본값 environment=='dev' 라 실함수가 True 를 반환해 동일 통과(테스트 수정 불필요). 의미 정합성을 더 엄격히 하려면 해당 patch 대상을 turn_runtime.runtime_fallback_allowed 로 바꾸는 선택적 후속 편집 가능.
|
||||
- affected_callers: ['apps/api/app/routes/sessions.py:928 submit_turn — _append_session_turn(counselor) 제거, record_completed_turn 으로 대체(같은 패치 포함)', 'apps/api/app/routes/sessions.py:941 submit_turn — _append_session_turn(client) 제거(대체됨)', 'apps/api/app/routes/sessions.py:956 submit_turn — _update_session_state 제거(대체됨)', 'apps/api/app/routes/sessions.py:1002 stream_turn — _append_session_turn->append_completed_turn(context 인자 추가)', 'apps/api/app/routes/sessions.py:1012 stream_turn — _update_session_state->update_session_state(context 인자 추가)', 'apps/api/app/routes/sessions.py:1014 stream_turn — _append_session_turn(client)->append_completed_turn(context 인자 추가)', 'apps/api/app/routes/sessions.py:688,764,900,976,1051 — _load_session_or_404 호출부: 시그니처/예외 동일, 무수정', 'apps/api/app/routes/voice.py:320,337,352 _run_turn_and_speak — _append_voice_turn/_update_voice_state 제거, record_completed_turn 으로 대체', 'apps/api/app/routes/voice.py:286 _run_turn_and_speak, :477 _bind_session — _load_voice_session 호출부: 시그니처 동일, 무수정', "apps/api/app/test_rbac_idor.py:149,191,238,302 patch.object(sessions,'runtime_fallback_allowed') — dev 기본값 덕에 통과 유지, 갱신 불필요(선택적)"]
|
||||
|
||||
- edits: 11건
|
||||
|
||||
|
||||
## RAG 활성화 설계
|
||||
|
||||
|
||||
### apps/api/app/routes/sessions.py — 라이브 턴/회기 파이프라인에 rag.py 배선(회상·KB 행동단서), graceful degradation 포함 (risk=medium)
|
||||
- 현재 sessions.py는 `memory.build_recall_context()`를 인자 없이 호출(항상 빈 회상)하고, prepare_turn에 kb_behavior_cues를 안 넘긴다. rag.py(search_kb/retrieve_persona_memory)는 routes/kb.py에서만 쓰이고 라이브 루프엔 미배선이다. orchestrator.prepare_turn → persona.build_turn_messages는 이미 recall_summary/pinned_facts/kb_behavior_cues를 L2로 주입하도록 end-to-end로 뚫려 있어, 라우트의 호출부만 채우면 활성화된다(시그니처 변경 0).
|
||||
|
||||
핵심 설계 결정:
|
||||
1) KB 행동단서는 페르소나 증상/호소를 질의로 search_kb(role=CLIENT)를 회기 시작 1회 산출해 캐시하고 매 턴 prepare_turn에 전달. CLIENT 정책(expose_body=False)이 본문을 잘라 behavior_cue만 돌려주므로 R4/M6(CCD 본문 비노출) 자동 보존. 질의 텍스트는 임베더/tsquery 입력일 뿐 프롬프트에 안 들어가므로 dsm5 차원 키를 질의로 써도 메타 누설 없음. L2가 cache=True라 회기 1회 로드가 캐시 친화(persona.py 주석과 정합).
|
||||
2) 회상은 sess 생성 후 case_id로 app.session_summary(직전 요약)+retrieve_persona_memory(episodic)를 합쳐 build_recall_context로 조립. init_state의 carry는 기존대로 유지(회귀 0; carry는 현재 session_no=1 고정이라 dormant).
|
||||
3) 모든 RAG 경로는 db.get_pool()(미초기화 RuntimeError)·rag.NotConfigured·DB 오류를 삼켜 빈 값 반환 → 상담 루프 비차단. routes/kb.py가 같은 예외를 503으로 올리는 것과 달리 라이브 루프는 graceful degradation이 계약(요구사항 3). 세 RAG 쿼리를 각각 별도 acquire로 분리해, 한 쿼리의 트랜잭션 abort가 다른 쿼리를 오염시키지 않게 함.
|
||||
|
||||
주의(검증 필요 가정): (a) M2 다회기 연속성이 실제로 작동하려면 동일 (learner,persona) 세션들이 같은 case_id를 공유해야 하는데, create_session은 세션마다 새 runtime_case_id를 만든다(InProcSession.case_id=runtime_case_id). 따라서 현재는 case 스코프 회상이 '정확하지만 비어 있음'(첫 회기)으로 degrade — 케이스 식별자 통합은 본 과제 범위 밖. (b) app.turn_embedding writer가 아직 없어 episodic recall은 dormant(빈 결과). (c) _hydrate의 app.turns.id(uuid PK) 컬럼명은 가정 — 틀리면 except로 []. (d) pinned_facts는 app.session_summary에 컬럼이 없어 현재 []이며, 압축 파이프라인이 pinned_facts를 적재하면 prev_summary 경유로 자동 채워지도록 forward-compatible하게 배선.
|
||||
- edits: 7건
|
||||
- 검증: 1) 회귀: `cd apps/api && python -m pytest app/test_orchestrator_masking.py app/test_session_turn_persistence.py -q` — kb_behavior_cues는 기본값 None인 추가 kwarg라 직접 prepare_turn을 부르는 기존 테스트는 영향 없음(통과 유지). 2) 전체: `cd apps/api && python -m pytest app -q` (77개) + `python -m pytest engine_gateway -q`. 3) graceful 단위 검증(신규 권장): DB 풀 미초기화 상태에서 `_retrieve_kb_behavior_cues(P1)`/`_build_start_recall(case_id=..., card=P1)`이 예외 없이 각각 []·빈 RecallContext를 반환하는지(monkeypatch 불필요 — get_pool RuntimeError 경로). 4) 통합 스모크: KB 인덱싱(POST /kb/index) 후 임베더 설치 환경에서 회기 시작 → submit_turn 응답이 200이고, persona.build_turn_messages가 L2에 '증상 행동단서' 블록을 포함하는지(ctx.messages 검사) — 미설치 환경에선 빈 cues로 200 유지. 5) import 무결성: `python -c "import app.main"`(rag 무거운 의존성은 함수 내부 import라 통과해야 함).
|
||||
|
||||
|
||||
### KB seed 콘텐츠
|
||||
All schema and policy details confirmed. Here is the seed KB design.
|
||||
|
||||
---
|
||||
|
||||
# Vignette KB 시드 콘텐츠 설계 — RAG 회수용 (스타터)
|
||||
|
||||
## 0. 사실 확인 (코드에서 검증한 계약)
|
||||
|
||||
`POST /kb/index` → `rag.index_document(conn, IndexRequest)` 의 **실제 입력 형태**는 `routes/kb.py`의 `IndexRequestIn` / `IndexChunkIn`이다.
|
||||
|
||||
```
|
||||
IndexRequestIn = { source_id, doc_uri, version=1, content_hash?, chunks: IndexChunkIn[] }
|
||||
IndexChunkIn = { seq:int(필수), chunk_text:str(필수,≥1), heading_path?, context_prefix?,
|
||||
kb_kind?, visible_to?:str[], sensitivity?:0..3, label_id?, meta?, token_count? }
|
||||
```
|
||||
|
||||
`index_document`이 `kb.chunk`에 적재하는 컬럼 매핑 (rag.py L775~799):
|
||||
- `visible_to` 미지정 → `COALESCE($10, ARRAY['client','counselor','evaluator'])` (전체 공개가 기본 — **반드시 명시**해서 정보비대칭을 강제해야 함)
|
||||
- `sensitivity` 미지정 → `COALESCE($11, 0)` (0=공개)
|
||||
- `kb_kind` 미지정 → `'theory'`
|
||||
- `context_prefix` → 색인 시 `prefix + body` 결합본을 임베딩/BM25 대상으로 쓰되, 런타임 LLM 주입 본문(`chunk_text`)에는 미포함 (Contextual Retrieval)
|
||||
|
||||
### 정책 4-튜플이 회수에 거는 제약 (rag.py POLICIES) — 시드 설계의 핵심 제약
|
||||
|
||||
| role | kinds 화이트리스트 | sens_max | expose_body | 회수 시 반환 |
|
||||
|---|---|---|---|---|
|
||||
| **client**(내담자) | diagnostic, theory, technique | **1** | False | `meta.behavior_cue` 우선, 없으면 본문 절단 |
|
||||
| **counselor**(상담사 보조) | theory, technique, microskill, ko_context | **0** | True | 본문+예시 |
|
||||
| **evaluator**(평가) | (전체) | 2 | True | 본문 + `label_id` + `meta.bias_weight` |
|
||||
|
||||
> **결론**: 시드의 `visible_to` + `sensitivity` + `kb_kind` 3개 컬럼이 "누가 무엇을 회수하느냐"를 DB WHERE로 강제한다. 시드를 잘못 태깅하면(예: 이론 본문에 `client` 포함) 정보비대칭이 깨진다.
|
||||
|
||||
### 선행 조건: `kb.source` 등록 (index_document은 source를 만들지 않음)
|
||||
|
||||
`kb.document.source_id`는 `kb.source(source_id)` FK다. `index_document`은 source를 생성하지 않으므로 **인덱싱 전에 source 행이 존재해야 한다**. license_class C/D(DSM verbatim·미성년 파생)는 `external_llm_ok=false`로 국내 라우팅 강제 — **아래 시드는 전부 합성/환언(A)이라 verbatim 저작권 위험 없음**.
|
||||
|
||||
---
|
||||
|
||||
## 1. `kb.source` 시드 (SQL 또는 부트스트랩 — index 전 1회)
|
||||
|
||||
```sql
|
||||
INSERT INTO kb.source (source_id, title, kb_kind, license_class, citation, external_llm_ok) VALUES
|
||||
('theory_pct_v1', '인간중심상담 핵심개념(환언 스타터)', 'theory', 'A',
|
||||
'임상팀 검수 전 개발 시드 — Rogers PCT 환언, verbatim 아님', TRUE),
|
||||
('theory_cbt_v1', 'CBT 핵심개념(환언 스타터)', 'theory', 'A',
|
||||
'임상팀 검수 전 개발 시드 — Beck/Martell 환언', TRUE),
|
||||
('technique_cbt_v1','CBT 기법 절차(환언 스타터)', 'technique', 'A',
|
||||
'임상팀 검수 전 개발 시드 — 절차 환언', TRUE),
|
||||
('persona_cues_v1', '시드 페르소나 행동단서(P1~P3)', 'diagnostic', 'A',
|
||||
'합성 페르소나 행동 표현 — DSM verbatim 아님, 본문 비노출', TRUE);
|
||||
```
|
||||
|
||||
> `kb.source.kb_kind`는 source당 1개(CHECK 제약)라 kb_kind별로 source를 분리했다. 행동단서는 증상 표현이므로 `diagnostic`.
|
||||
|
||||
---
|
||||
|
||||
## 2. 이론 지식 청크 — 상담사/평가 AI 회수용 (`sensitivity=0`, `visible_to=['counselor','evaluator']`)
|
||||
|
||||
설계 원칙:
|
||||
- `visible_to`에서 **client 제외** → 내담자 AI가 이론을 "학습"해 메타발화하는 누설 차단(R4).
|
||||
- `sensitivity=0` 필수 → counselor 정책이 `sens_max=0`이라 0만 회수.
|
||||
- `context_prefix`로 doc 맥락 1문장 부여(동음이의·짧은 청크 회수 정확도↑).
|
||||
|
||||
### 2-A. 인간중심(PCT) — `POST /kb/index` 바디 예시
|
||||
|
||||
```json
|
||||
{
|
||||
"source_id": "theory_pct_v1",
|
||||
"doc_uri": "pct/core-concepts.md",
|
||||
"version": 1,
|
||||
"chunks": [
|
||||
{
|
||||
"seq": 0,
|
||||
"kb_k
|
||||
132
docs/ops/source-docs-gap-analysis-2026-06-26.md
Normal file
132
docs/ops/source-docs-gap-analysis-2026-06-26.md
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
# Vignette 원천문서 갭 분석 종합 (2026-06-26)
|
||||
|
||||
> **SSOT 주의**: 결론·상태의 권위 기준은 `docs/dev_dashboard.html`이다. 이 문서는 그 SSOT 항목의
|
||||
> **상세 근거**다. 대시보드 "원천문서 갭" 항목과 함께 본다.
|
||||
|
||||
## 메타
|
||||
|
||||
- **원천문서 5종**(한신대 산학협력, 구훈정 교수): doc1 청소년·대학생 사례 워크북(3·4장), doc2 인간중심접근 사례·프로토콜(4장), doc3 산학협력 회의록(2026-06-08), doc4 산학협력 신청서(공식 계약), doc5 사례개념화 워크북(3장, 상호작용 분석 추가본).
|
||||
- **방법**: 각 문서를 PDF로 변환해 시각 정독(에이전트 1/문서) + Vignette 코드베이스 4개 도메인 조사 → 1차 종합 → 적대적 비평 → 최종 재구성. (워크플로우 `source-docs-gap-analysis`, 에이전트 12, 토큰 ~1.11M.)
|
||||
- **변환 산출물**(scratchpad): `docs-analysis/pdf/doc{1..5}-*.pdf`, `txt/*.md|txt`, `hwphtml/doc4*/`(신청서 폼 이미지). 원본 raw 결과: `tasks/wrimqxyqf.output`.
|
||||
|
||||
## 검증 메모 (정직성)
|
||||
|
||||
최종 보고서의 "현재상태"는 조사 에이전트의 grep/코드 판독을 적대적 비평이 인용한 것이다. 보고서 스스로
|
||||
"착수 전 코드에서 1차 재확인 권고"라고 명시한다. **아래 핵심 주장은 작성자(메인 루프)가 직접 grep으로 재확인했다:**
|
||||
|
||||
| 주장 | 직접 검증 결과 |
|
||||
|---|---|
|
||||
| C1 사례개념화/인지삼제/4사분면 구조 부재 | `cognitive_triad/case_conceptual/인지삼제/4사분면/quadrant` grep **0건** ✓ |
|
||||
| C2 위기 escalate가 safety_events 미적재 | `orchestrator.py:281` escalate 시 `StreamEvent("safety")`만 yield, DB insert·교수자 알림 **없음** ✓ (stream 신호는 존재) |
|
||||
| C3 이론모드 미주입 | `services/persona.py:147 build_turn_messages` 시그니처에 `theory` 인자 **없음** + `Session.tsx:589` `"humanistic"` 하드코딩 ✓ |
|
||||
| H2 턴별 평가 미가동 | `make_eval_hook` 정의·export(`evaluator.py:738`)됐으나 `routes/`에서 주입 **0건** ✓ |
|
||||
| H3 페르소나 저작 경로 부재 | `persona_repository.py`는 in-code `SEED_PERSONAS`(P1~P3)만 materialize, draft CRUD·외부 JSON 로드 **없음** ✓ |
|
||||
|
||||
그 외 항목(H1 KPI 수집, H4 한국어 PII, M1~M3, X1~X2, L1)은 분석 결과이며 착수 전 코드 재확인 권고. doc4 KPI 범위(κ/ICC·환각률)는 신청서 본문 미명시 — 별도 평가설계 문서 근거 가능성(과대해석 금지).
|
||||
|
||||
---
|
||||
|
||||
## 1. 총평
|
||||
|
||||
원천문서가 요구하는 다수 차원 대비 Vignette가 **가장 부족한 핵심 3가지**:
|
||||
|
||||
1. **임상 산출물 구조의 전면 부재** — doc1/doc4/doc5가 1급 산출물로 요구하는 사례개념화 워크시트(11탐색항목·호소 5영역·인지삼제·1·2차 감정·보호/방해 4사분면·생물심리사회 목표)가 코드·스키마에 전무.
|
||||
2. **위기개입 안전 프로토콜의 미구현** — IRB 100% 준수 전제인 비밀보장 예외 고지·자살사고 단계적 탐색·생명유지서약·에스컬레이션이 없고, escalate=True여도 safety_events 적재·교수자 알림이 없음.
|
||||
3. **계약 평가 KPI 수집 체계의 부재** — doc4가 못박은 자기효능감·기술숙련도·수련만족도 3종 사전사후 비교와 20명 실험/통제군 설계가 코드에 0건.
|
||||
|
||||
## 2. 원천문서 5종 요지
|
||||
|
||||
- **doc1 — 청소년·대학생 사례 워크북(3·4장)**: 첫 회기 축어록→사례개념화→목표·전략 표준 분석틀 + 두 사례(비자발 청소년/자발 대학생). *함의*: 페르소나 완성형 스펙, openness 곡선 정량 근거, 위기 프로토콜·다회기 아크(2~10회기), 회기리뷰 주석 포맷의 그라운드 트루스.
|
||||
- **doc2 — 인간중심접근 사례·프로토콜(4장)**: 미혼모 3회기 상담의 인간중심 사례개념화 정답지. *함의*: theory_mode=humanistic 1급 필드, 공감·반영 정확도 기반 openness, 회기리뷰 채점 루브릭 직접 근거.
|
||||
- **doc3 — 산학협력 회의록(2026-06-08)**: 3주체(임상팀 구훈정·어유경 / 트웬티온스 / 사업단 김시윤) 협력·거버넌스 확정. *함의*: '콘텐츠·평가기준=임상팀 소유, 구현=기술팀' R&R, 평가 루브릭은 임상팀이 외부 정의·수정 가능해야 함, AI API 비용을 운영 리스크로 명시.
|
||||
- **doc4 — 산학협력 신청서(공식 계약)**: 기간(2026.5.18~9.30, 20주)·예산·평가지표·산출물·서약의 권위 원천. *함의*: humanistic+CBT 2종 필수 이론을 '단계적 프롬프트 체인'으로 계약, 3척도 사전사후·20명 실험/통제군·단회기 50분, 9월 저작권 등재, IRB 100% 준수, 기술스택 명시(Spring Boot 3/Node.js·TimescaleDB — 실제 FastAPI/Python과 불일치).
|
||||
- **doc5 — 사례개념화 워크북(3장, 상호작용 분석 추가본)**: 비자발 청소년(자살사고) 축어록(상1~63)에 기법 코딩. *함의*: 회기리뷰 루브릭·기법 코딩 택소노미(κ/ICC 스킴)·페르소나 사양·종결 규칙의 직접 근거.
|
||||
|
||||
## 3. 부족한 부분 — 심각도 우선순위
|
||||
|
||||
### [critical] C1. 사례개념화·치료계획 산출물 구조 전면 부재
|
||||
- **근거**: doc1, doc4(mockup '상담 설정·피드백 리포트'), doc5
|
||||
- **현재상태**(✓직접검증): 사례개념화/cognitive_triad/4사분면 grep 0건. CCD는 숨겨진 정답키일 뿐 학습자 산출물 폼 아님.
|
||||
- **권고**: 사례개념화 워크시트 스키마(11항목/5영역/인지삼제/4사분면/생물심리사회 목표)를 입력 폼으로, AI가 축어록에서 신념·감정·욕구 초안 추출·제안, 규칙 기반 채점. 루브릭은 임상팀이 외부 설정으로 정의 가능하게 외부화.
|
||||
|
||||
### [critical] C2. 위기개입 프로토콜·생명유지서약·에스컬레이션 미구현
|
||||
- **근거**: doc1, doc5(핵심 훈련 시나리오), doc4(IRB 전제)
|
||||
- **현재상태**(✓직접검증): escalate=True 시 client stream 이벤트만, safety_events insert·교수자 알림 코드 없음. prepare_turn이 risk_level을 state_machine.evolve의 ideation_observed로 미전달.
|
||||
- **권고**: (1) 위기 분기 상태(예외고지→단계적 탐색→서약 유도)를 상태머신에 추가, (2) escalate 시 safety_events insert + 교수자 알림, (3) 학습자 위기탐색 누락 시 회기리뷰 감점, (4) ideation_observed 전달.
|
||||
|
||||
### [critical] C3. 이론모드 미주입 + CBT 콘텐츠 자체 부재
|
||||
- **근거**: doc4(humanistic+CBT 필수), doc2/doc5
|
||||
- **현재상태**(✓직접검증): build_turn_messages에 theory 인자 없음, Session.tsx:589 'humanistic' 하드코딩. CBT 프롬프트 체인·이론부합 루브릭 전무.
|
||||
- **권고**: theory 인자 추가 + 이론별 단계적 프롬프트 체인(공감·반영 / 인지재구조화·행동활성화) + 프론트 이론 선택 UI + 이론 부합도 스코어링 루브릭.
|
||||
|
||||
### [high] H1. 계약 평가 KPI(자기효능감·기술숙련도·수련만족도 사전사후) 수집·집계 전무
|
||||
- **근거**: doc4(20명 실험/통제군·단회기 50분·3척도 pre-post)
|
||||
- **현재상태**: 자기효능감/사전사후/수련만족/실험통제군 grep 0건. Phase3 KPI도 report shape만, 계산 코드 0줄.
|
||||
- **권고**: 3척도 pre-post 폼, 실험/통제군 배정, 자동누적 대시보드, 추이 시각화, 검정 계산 코드. (κ/ICC·환각률은 doc4 미명시 — 평가설계 문서 확정 필요.)
|
||||
|
||||
### [high] H2. 턴별 회기 리뷰(상호작용 분석 2열) 미가동
|
||||
- **근거**: doc2, doc5(골드 포맷: 기법 태깅+적절성+대안반응+이론해석)
|
||||
- **현재상태**(✓직접검증): make_eval_hook 정의·export됐으나 라우트 미주입 → 턴별 fast-loop 미동작(회기종료 deep-loop만). few-shot 골든셋 기본 OFF.
|
||||
- **권고**: eval_hook을 turn 파이프라인에 주입. 회기리뷰 UI 좌(축어록 타임라인+비언어)/우(기법·적절성·대안반응·이론) 2열. 원천 축어록을 채점 few-shot 골든셋으로 적재.
|
||||
|
||||
### [high] H3. 임상팀 콘텐츠 입력 경로(페르소나 저작 CRUD) 부재 + P4~P7 미적재
|
||||
- **근거**: doc3(R&R), doc4(페르소나=전문가 산출물)
|
||||
- **현재상태**(✓직접검증): personas 라우트에 검수 승인/반려만, draft 생성·편집 API 없음. persona_repository는 in-code SEED(P1~P3)만, 외부 JSON 미로드.
|
||||
- **권고**: 페르소나 저작 CRUD(draft→review)와 P4~P7 적재, 루브릭·이론 콘텐츠를 임상팀 편집 가능 데이터로 외부화.
|
||||
|
||||
### [high] H4. PII 마스킹 한국어 공백 + 외부전송 관측 부재
|
||||
- **근거**: doc1/2/5(실명·날짜·장소·금액·미성년·자살시도 다수), doc4(IRB·개인정보)
|
||||
- **현재상태**(분석): Presidio language='en' 고정으로 한국어 이름/주소/기관 미탐지, 폴백은 번호·이메일만. audit.llm_call_log 0행(런타임 미관측). consent_at 컬럼만, 동의 수집/게이트/철회 엔드포인트 전무.
|
||||
- **권고**: 한국어 PII 탐지 추가, 마스킹 미들웨어 하드게이트화, llm_call_log 적재로 런타임 관측, 미성년/guardian 동의 수집·하드게이트·철회.
|
||||
|
||||
### [medium] M1. 비언어/준언어 임상 이벤트 캡처·태깅 부재
|
||||
- **근거**: doc1/2/5(침묵 초·한숨·울음·떨림을 1급 단서)
|
||||
- **현재상태**(분석): voice.py는 EOT용 silence_ms만, 한숨/울음/침묵 타임스탬프 이벤트 캡처·리뷰 표시 전무. 서버 RMS 힌트 프론트 미사용(dead).
|
||||
- **권고**: 침묵·한숨·울음을 타임스탬프 메타 이벤트로 보존·시각화, '침묵 견디기'를 역량 지표화, 페르소나 의도적 침묵·비유창 한국어 렌더링.
|
||||
|
||||
### [medium] M2. 다회기 종단 케이스 아크·교차회기 사례개념화 미구동
|
||||
- **근거**: doc1(2~10회기), doc2(3회기 자기개념 진화)
|
||||
- **현재상태**(분석): case_state.ccd_estimate/presenting_arc/alliance_level 스키마만, build_recall_context() 빈 컨텍스트 반환. 음성 경로 빈 RecallContext.
|
||||
- **권고**: case_state 런타임 구동, build_recall_context 실제 회상/pinned_fact 주입, 접수면접→다회기 연속성·자기개념 진화.
|
||||
|
||||
### [medium] M3. SSO claim 매핑·식별자 안정성·deprovisioning 감사 미연결
|
||||
- **근거**: doc3(교내 SSO), doc4(RBAC·익명화)
|
||||
- **현재상태**(분석): Google/SAML 콜백 cohort_ids=[] 하드코딩, role이 email allowlist, external_id가 'email:{}' 파생(불안정). SAML 서명검증 미구현. role변경/삭제 audit 미기록. 거버넌스 증거(claim-map·동의 매트릭스) 부재.
|
||||
- **권고**: SSO claim→role/cohort/institution_user_id 매핑+안정 식별자(sub/NameID), SAML 서명검증, deprovisioning audit, 한신 IdP 확정·외부 증거 수급.
|
||||
|
||||
### [medium/추적] X1. 재귀학습·데이터셋 export 파이프라인 미구현
|
||||
- **현재상태**(분석): ds.* 스키마(κ/ICC 컬럼)만, read/write 코드 0건. JSONL export·IAA 게이트·골든셋 승격 미코딩.
|
||||
- **권고**: JSONL export 잡, IAA 게이트(κ≥0.6/ICC≥0.75), 골든셋 승격.
|
||||
|
||||
### [medium/추적] X2. AI API 비용 관측 부재
|
||||
- **근거**: doc3(회의록이 'AI API 비용'을 운영 리스크로 명시)
|
||||
- **현재상태**(분석): 게이트웨이 토큰 텔레메트리 0 고정, cost 모니터링·캐싱 부재.
|
||||
- **권고**: 토큰 텔레메트리 실측, 저비용 모델 분기·캐싱, 비용 대시보드.
|
||||
|
||||
### [low] L1. 기술스택 신청서-구현 불일치 및 단기일정 산출물 압박
|
||||
- **근거**: doc4(Spring Boot 3/Node.js·TimescaleDB vs 실제 FastAPI/Python), doc3(20주)
|
||||
- **권고**: 스택 정합 또는 변경 사유를 거버넌스 회의록으로, 9월 저작권 등재 문서화 수준을 일정 반영.
|
||||
|
||||
## 4. 즉시 착수 가능 vs 소유자 결정/외부 의존
|
||||
|
||||
### A. 즉시 착수 가능(코드/스키마 내부, 외부 합의 불요)
|
||||
- C2(2),(4): escalate 시 safety_events insert, prepare_turn에서 ideation_observed 전달 — 배선 수정.
|
||||
- C3: theory 인자 추가 + Session.tsx 하드코딩 제거 + 프론트 선택 UI 골격.
|
||||
- H2: eval_hook을 turn 파이프라인에 주입(이미 정의·export됨) — 배선.
|
||||
- H3: 페르소나 저작 CRUD + P4~P7 로드 경로.
|
||||
- H4(부분): Presidio 한국어 설정·룰, llm_call_log 적재, 마스킹 미들웨어 하드게이트.
|
||||
- M1(부분), M2, X1, X2: 메타 이벤트 보존, build_recall_context 구현, ds.* read/write·export, 토큰 텔레메트리.
|
||||
|
||||
### B. 소유자 결정 / 외부(임상팀·기관) 의존
|
||||
- **임상팀(구훈정·어유경) 산출물**: C1 워크시트 항목·채점 루브릭, C3 CBT 이론 콘텐츠·프롬프트 체인, C2 위기 스크립트·서약 문안, H2 골든셋 코딩 — 콘텐츠는 doc3 R&R상 임상팀 소유. 코드는 '편집 가능 구조'만 선제 구축.
|
||||
- **소유자 평가설계 결정**: H1 실험/통제군 배정·3척도 문항·50분 흐름. κ/ICC·환각률 목표는 doc4 미명시 → 평가설계 문서 확정.
|
||||
- **기관(한신 IT) 의존**: M3 SSO IdP 프로토콜·test tenant·claim 스키마, SAML 인증서, 거버넌스 증거.
|
||||
- **거버넌스 결정**: L1 스택 정합성 처리 방향, 저작권 등재 문서화 수준, IP 협의.
|
||||
|
||||
## 5. 미해결·판독 한계 (quality_flags)
|
||||
|
||||
- 원천 핵심 축어록 2단 표(doc5 8~28p 상1~63, doc2 3회기 표)는 txt 추출 시 일부 누락 — PDF 시각 판독이 1차 근거.
|
||||
- OCR 잔재(doc1 '괴게 씩씩'≈'되게 씩씩', doc5 따옴표 깨짐·오탈자). 의미는 시각 보정했으나 일부 표현 불확실.
|
||||
- doc4 KPI 범위 주의: κ/ICC·환각률은 신청서 본문 미명시('정량 신뢰도 확보' 수준) — 계약 확정 지표로 단정 금지.
|
||||
- doc4/doc3 행정 불일치: 참여교수 1명 vs 구훈정·어유경 2명 병기, 서식7 연도 '2025' 오기, 연구책임자 표기 불일치(류승택/구훈정), 트웬티온스 4인 성명 공란.
|
||||
- '현재상태'는 grep/코드 사실 기반 적대적 비평 인용 — 위 검증 메모 5건은 작성자 직접 재확인. 그 외는 착수 전 코드 1차 재확인 권고.
|
||||
Loading…
Add table
Add a link
Reference in a new issue