@ -741,7 +741,7 @@ COUNSELING PRACTICE · 4주차 ← kicker
핵심: **운영 신뢰성 + 감사.** 학습 내용엔 개입 안 하고 시스템·사용·교수활동 감사에 집중. 무채 슬레이트로 가장 차분.
핵심: **운영 신뢰성 + 감사.** 학습 내용엔 개입 안 하고 시스템·사용·교수활동 감사에 집중. 무채 슬레이트로 가장 차분.
- **정보구조:**`/admin`은 운영 홈으로 고정한다. 사용자 CRUD는 `/admin/users`, 역할·그룹·권한 정책은 `/admin/access`, 사용자 문제·불만·장애 접수는 `/admin/tickets`로 분리한다. 설정은 계속 `/settings`다.
- **정보구조:**`/admin`은 운영 홈으로 고정한다. 사용자 CRUD는 `/admin/users`, 역할·그룹·권한 정책은 `/admin/access`, 사용자 문제·불만·장애 접수는 `/admin/tickets`로 분리한다. 설정은 계속 `/settings`다.
- **증거 원칙:** 운영 티켓은 `app.support_ticket`, 헬스 샘플 이력은 `app.admin_health_event`에 저장된 실제 행만 표시한다. 상시 모니터링 데몬이 없는 현재 단계에서는 "SLA 업타임"처럼 표현하지 않고 "최근 헬스 샘플 정상률/중단 기록"으로 표시한다. 세분 그룹 CRUD처럼 저장소/API가 없는 항목은 빈 상태 또는 정책 초안으로 남기며, 운영 수치처럼 보이는 목업 숫자는 금지한다.
- **증거 원칙:** 운영 티켓은 `app.support_ticket`, 헬스 샘플 이력은 raw `app.admin_health_event`와 daily `app.admin_health_daily_rollup`에 저장된 실제 행만 표시한다. 티켓 중복 표시는 저장된 `fingerprint`/`parent_ticket_id` 기반 힌트와 관리자 수동 연결만 보여주며, 자동 병합이나 자동 수정처럼 보이게 만들지 않는다. 관리자 화면 조회와 synthetic sampler가 샘플을 적재할 수 있지만, "SLA 업타임"처럼 표현하지 않고 "최근 헬스 샘플 정상률/중단 기록"으로 표시한다. 세분 그룹 CRUD처럼 저장소/API가 없는 항목은 빈 상태 또는 정책 초안으로 남기며, 운영 수치처럼 보이는 목업 숫자는 금지한다.
- **운영 KPI 5:** 사용자/강좌/세션주/가동%/API오류. mono 32px. 오류 0 초과 시 해당 KPI만 `--warn-solid`, 임계 초과 `--crit-solid`. 정상 땐 전부 무채 — "조용한 게 정상".
- **운영 KPI 5:** 사용자/강좌/세션주/가동%/API오류. mono 32px. 오류 0 초과 시 해당 KPI만 `--warn-solid`, 임계 초과 `--crit-solid`. 정상 땐 전부 무채 — "조용한 게 정상".
- **세션 추이 = 단일 라인차트**(막대 아님). 피크 1점만 라벨.
- **세션 추이 = 단일 라인차트**(막대 아님). 피크 1점만 라벨.
- **음성·AI 사용 + 비용:** OpenAI 음성 분/평가 토큰/추정 비용/예산 대비 게이지(80% 넘으면 `--warn-solid`). 운영비 가시화(윤찬 graphify 비용 통제 문화).
- **음성·AI 사용 + 비용:** OpenAI 음성 분/평가 토큰/추정 비용/예산 대비 게이지(80% 넘으면 `--warn-solid`). 운영비 가시화(윤찬 graphify 비용 통제 문화).
@ -25,19 +25,53 @@ The first implementation boundary is the engine gateway contract:
- `POST /v1/stream`
- `POST /v1/stream`
- SSE events: `token`, `done`, `error`
- SSE events: `token`, `done`, `error`
- request/response models owned by `apps/api/app/contracts/engine_gateway.py`
- request/response models owned by `apps/api/app/contracts/engine_gateway.py`
- gateway SSE line parsing owned by `EngineGatewaySseLineDecoder` behind `EngineClient.stream_packets()`
Both the FastAPI engine client and the current Python engine gateway import this same contract. A future Node.js gateway must preserve these shapes.
Both the FastAPI engine client and the current Python engine gateway import this same contract. The API orchestrator consumes decoded `EngineGatewaySsePacket` values from `EngineClient.stream_packets()` instead of hand-parsing `event:`/`data:` lines. The current Python gateway also returns `/v1/generate` through `GenerateResponse.model_dump()` rather than a hand-mirrored dict literal. A future Node.js gateway must preserve these shapes.
The cross-runtime golden fixture is `apps/api/engine_gateway/golden/engine_gateway_contract.v1.json`, backed by `apps/api/engine_gateway/golden/engine_gateway_schema.v1.json`.
The fixture pins a representative `/v1/generate` request/response, stream `token/done/error` frames, decoded packet payloads, and the provider `[DONE]` compatibility line. The schema artifact pins the same request/response/event shapes without requiring Python imports. The Python contract test validates that the schema artifact matches the current Pydantic contract and that the fixture passes both JSON Schema and the runtime SSE decoder. `scripts/check-engine-gateway-contract.mjs` reads the same artifacts from Node.js, decodes the SSE frames, ignores provider `[DONE]` compatibility lines, and proves packet-level equivalence without importing Python code. A future Node.js gateway should keep this runner green and then add live endpoint conformance on top of it.
Provider pass-through sentinels such as `data: [DONE]` are ignored compatibility lines, not completion packets. A Node.js gateway must translate provider completion into the explicit `event: done` JSON frame above.
## Invariants
## Invariants
- `/personas` normal state remains `source:"database", degraded:false`; `seed_fallback` is not normal.
- `/personas` normal state remains `source:"database", degraded:false`; `seed_fallback` is not normal.
- Browser credentials and BFF session behavior stay unchanged.
- Browser credentials and BFF session behavior stay unchanged.
- Browser-facing `/sessions/{id}/stream` SSE stays a separate app contract; the engine gateway stream uses JSON token payloads.
- Browser-facing session list/dashboard/detail/review/share response DTOs stay stable across the Python route and any future Node read API.
- RBAC, RLS, audit logging, and PII masking stay fail-closed.
- RBAC, RLS, audit logging, and PII masking stay fail-closed.
- Existing ports and local/prod startup paths stay compatible until a service is deliberately replaced.
- Existing ports and local/prod startup paths stay compatible until a service is deliberately replaced.
## Second Boundary
The second internal boundary is the browser-facing session read model.
`apps/api/app/session_read_model.py` now owns the session list, learner dashboard, session detail, session review, worksheet, teacher-review status, and share payload DTOs plus deterministic builders. `apps/api/app/routes/sessions.py` keeps route decorators, auth/role checks, consent/onboarding gates, RLS-backed persistence reads, mutations, streaming, and session lifecycle behavior.
This preserves the current FastAPI delivery path while making a future Node.js read API mirror one module's browser contract instead of re-deriving response shapes from mixed route code. Hidden/evaluator-only turn filtering, saved worksheet precedence, teacher read-only review status, and public share sanitization remain invariants.
- `apps/api/engine_gateway/golden/engine_gateway_contract.v1.json` and `apps/api/engine_gateway/golden/engine_gateway_schema.v1.json` validated by `engine_gateway/test_gateway_model.py`
<h1>지금 위치: <em>Phase 0은 public /turn proof만 남음</em>.<br><em>Phase 1 MVP 코어 루프</em>는 로컬 DB-backed 실증 완료.</h1>
<pclass="pulse-state">코어(저항·반응 조절, 상태머신, RAG, 이론모드, 가드레일, 평가 JSON Schema)는 <b>잘 정렬</b>됐고, 이번 점검에서 학습자 홈·교수자 리뷰·공개/Tailnet 런타임 스모크도 녹색이다. 막힌 건 두 종류 — <b>실제 계정·외부 DNS·기관 결정이 있어야 풀리는 게이트</b>와, <b>임상 루브릭·콘텐츠·운영 증거처럼 아직 남은 제품 갭</b>이다. 이 판은 그 둘을 분리해서 보여준다.</p>
<pclass="pulse-state">코어(저항·반응 조절, 상태머신, RAG, 이론모드, 가드레일, 평가 JSON Schema)는 <b>잘 정렬</b>됐고, 이번 점검에서 학습자 홈·교수자 리뷰·공개/Tailnet 런타임 스모크도 녹색이다. 막힌 건 두 종류 — <b>실제 계정·외부 DNS·기관 결정이 있어야 풀리는 게이트</b>와, <b>임상 루브릭·콘텐츠·운영 증거처럼 아직 남은 제품 갭</b>이다. 이 판은 그 둘을 분리해서 보여준다.</p>
<divclass="phase-rail"id="phase-rail"aria-label="Phase 진행 추정"></div>
<divclass="phase-rail"id="phase-rail"aria-label="Phase 진행 추정"></div>
<pclass="dg-note">6차 적용(2026-06-27): 설정 화면은 글로벌 셸을 숨기고 설정 전용 좌측 레일을 주 내비로 승격했다. 계정, AI 운영, 테마, 음성, 알림 카드를 시안의 차분한 설정 워크스페이스 흐름에 맞춰 단일 컬럼으로 재배치했고, 모바일은 상단 고정 섹션 탭으로 모든 항목이 잘리지 않게 정리했다.</p>
<pclass="dg-note">6차 적용(2026-06-27): 설정 화면은 글로벌 셸을 숨기고 설정 전용 좌측 레일을 주 내비로 승격했다. 계정, AI 운영, 테마, 음성, 알림 카드를 시안의 차분한 설정 워크스페이스 흐름에 맞춰 단일 컬럼으로 재배치했고, 모바일은 상단 고정 섹션 탭으로 모든 항목이 잘리지 않게 정리했다.</p>
<pclass="dg-note">7차 적용(2026-06-27): 로그인 화면은 별도 생성한 래스터 상담실 배경(<code>login-counseling-room.png</code>)을 왼쪽 브랜드 패널에 적용해 시안의 실제 상담실 분위기를 반영했다. 학습자 홈은 모바일 grid 행 계산 때문에 생기던 리스트-상세 간 큰 공백을 제거하고, 회기 준비 보드가 한 흐름으로 이어지게 정리했다.</p>
<pclass="dg-note">7차 적용(2026-06-27): 로그인 화면은 별도 생성한 래스터 상담실 배경(<code>login-counseling-room.png</code>)을 왼쪽 브랜드 패널에 적용해 시안의 실제 상담실 분위기를 반영했다. 학습자 홈은 모바일 grid 행 계산 때문에 생기던 리스트-상세 간 큰 공백을 제거하고, 회기 준비 보드가 한 흐름으로 이어지게 정리했다.</p>
<pclass="dg-note">8차 적용(2026-06-27): 다크 테마가 라이트 primitive 토큰과 역할 accent에 덮여 흰 섹션이 섞이던 문제를 수정했다. 앱 부트 시 테마를 먼저 적용하고, 다크 모드에서 <code>paper/surface/hair/ink</code> primitive까지 스왑한다. 상담 세션 진행 화면은 테마 토글과 무관하게 어두운 콘솔 패널로 고정했고, 드래그형 종료 제스처는 명시적 <code>회기 종료</code> 버튼 + 확인 다이얼로그로 교체했다. 회기 리뷰도 동일한 다크 카드/축어록/워크시트 재질로 보정했다.</p>
<pclass="dg-note">8차 적용(2026-06-27): 다크 테마가 라이트 primitive 토큰과 역할 accent에 덮여 흰 섹션이 섞이던 문제를 수정했다. 앱 부트 시 테마를 먼저 적용하고, 다크 모드에서 <code>paper/surface/hair/ink</code> primitive까지 스왑한다. 상담 세션 진행 화면은 테마 토글과 무관하게 어두운 콘솔 패널로 고정했고, 드래그형 종료 제스처는 명시적 <code>회기 종료</code> 버튼 + 확인 다이얼로그로 교체했다. 회기 리뷰도 동일한 다크 카드/축어록/워크시트 재질로 보정했다.</p>
<pclass="dg-note">9차 적용(2026-06-27): 관리자 콘솔 IA를 <code>/admin</code> 운영 홈과 <code>/admin/users</code> 사용자 관리, <code>/admin/access</code> 접근 권한, <code>/admin/tickets</code> 운영 티켓 메뉴로 분리했다. 운영 홈은 실제 API가 있는 교육 진행·온라인 사용자·헬스·AI 비용·서비스 리소스를 우선 표시하고, 티켓/업타임 이력은 <code>app.support_ticket</code>·<code>app.admin_health_event</code> 기반 1차 저장 모델로 연결했다. 세분 그룹 권한 enforcement는 아직 정책 화면이며 가짜 권한 효과를 표시하지 않는다.</p>
<pclass="dg-note">9차 적용(2026-06-27): 관리자 콘솔 IA를 <code>/admin</code> 운영 홈과 <code>/admin/users</code> 사용자 관리, <code>/admin/access</code> 접근 권한, <code>/admin/tickets</code> 운영 티켓 메뉴로 분리했다. 운영 홈은 실제 API가 있는 교육 진행·온라인 사용자·헬스·AI 비용·서비스 리소스를 우선 표시하고, 티켓/업타임 이력은 <code>app.support_ticket</code>·<code>app.admin_health_event</code> 기반 저장 모델로 연결했다. 세분 그룹 권한 enforcement는 아직 정책 화면이며 가짜 권한 효과를 표시하지 않는다.</p>
<pclass="dg-note">17차 적용(2026-06-28): 운영 홈의 티켓 요약은 <code>resolved</code>/<code>closed</code> 이력을 진행 중 위험으로 세지 않는다. <code>high_priority_count</code>와 미니 티켓 목록은 미해결 티켓 기준이며, E2E 운영 티켓 테스트는 제목·본문에 테스트임을 명시하고 실패 시에도 cleanup patch로 해결 처리한다.</p>
<pclass="dg-note">17차 적용(2026-06-28): 운영 홈의 티켓 요약은 <code>resolved</code>/<code>closed</code> 이력을 진행 중 위험으로 세지 않는다. <code>high_priority_count</code>와 미니 티켓 목록은 미해결 티켓 기준이며, E2E 운영 티켓 테스트는 제목·본문에 테스트임을 명시하고 실패 시에도 cleanup patch로 해결 처리한다.</p>
<pclass="dg-note">19차 적용(2026-06-28): <code>/admin/tickets</code>는 현재 범위를 미해결 큐 목록과 상태 변경으로 좁혔다. 정적 분류/인사이트 탭은 제거하고, 카드에 카테고리·신고자·경로·접수/갱신 시각을 표시한다. 해결된 항목은 최근 해결 이력으로 내려간다. 필터·카테고리 큐·Claude Recipe headless 자동 처리·이슈 등록은 백로그 <code>운영 티켓 자동 분류·처리 2차</code>로 남겼다.</p>
<pclass="dg-note">19차 적용(2026-06-28): <code>/admin/tickets</code>는 현재 범위를 미해결 큐 목록과 상태 변경으로 좁혔다. 정적 분류/인사이트 탭은 제거하고, 카드에 카테고리·신고자·경로·접수/갱신 시각을 표시한다. 해결된 항목은 최근 해결 이력으로 내려간다. 필터·카테고리 큐는 이후 26차에서 처리했고, Claude Recipe headless 자동 처리·이슈 등록은 백로그 <code>운영 티켓 자동 분류·처리 2차</code>로 남겼다.</p>
<pclass="dg-note">26차 적용(2026-06-28): <code>/admin/tickets</code>에 검색, 상태·카테고리·우선순위·담당그룹·정체 필터와 카테고리 quick queue를 추가했다. 서버는 동일 query를 DB에서 처리하고, 티켓 접수 및 관리자 수동 처리 변경은 <code>audit.audit_log</code>에 metadata-only로 남긴다. 중복 병합, Claude Recipe 자동 수정 후보, 이슈/PR/작업 스레드 생성은 운영자 승인 전까지 계속 후속으로 둔다.</p>
<pclass="dg-note">26차 적용(2026-06-28): <code>/admin/tickets</code>에 검색, 상태·카테고리·우선순위·담당그룹·정체 필터와 카테고리 quick queue를 추가했다. 서버는 동일 query를 DB에서 처리하고, 티켓 접수 및 관리자 수동 처리 변경은 <code>audit.audit_log</code>에 metadata-only로 남긴다. Claude Recipe 자동 수정 후보, 이슈/PR/작업 스레드 생성은 운영자 승인 전까지 계속 후속으로 둔다.</p>
<pclass="dg-note">28차 적용(2026-06-28): 학습자 Settings에 <code>지원 요청</code> 섹션을 추가했다. <code>GET /users/support-tickets</code>는 현재 사용자 <code>reporter_id</code>의 티켓만 최신순으로 반환하고, 본문·신고자 이메일·신고자 이름은 응답에서 제외한다. Settings는 상태·카테고리·우선순위·담당 그룹·해결 메모만 읽기 전용으로 보여주며, 조회 실패는 설정 전체 로딩을 막지 않는다.</p>
<pclass="dg-note">29차 적용(2026-06-28): <code>/admin/health</code>와 synthetic sampler가 같은 <code>record_admin_health_sample()</code> 경로를 사용한다. <code>scripts/record-admin-health-sample.py</code>는 브라우저 admin 세션 없이 <code>app.admin_health_event</code>에 서비스별 샘플을 append하고, <code>scripts/install-health-sampler-task.ps1</code>는 Windows Scheduled Task로 주기 실행을 설치한다. 최신 로컬 one-shot은 <code>recorded_count=5</code>, <code>status=ok</code>, <code>engine_mode=claude_cli</code>다.</p>
<pclass="dg-note">30차 적용(2026-06-28): 헬스 샘플 이력은 raw <code>app.admin_health_event</code>와 daily <code>app.admin_health_daily_rollup</code>으로 분리했다. <code>scripts/maintain-admin-health-events.py</code>는 <code>--rollup-days</code>/<code>--retention-days</code> 명시 인자를 요구하고 기본 dry-run으로 동작한다. apply 모드는 rollup upsert 후 retention window 밖 raw row만 삭제하며, non-dev apply는 추가 확인 플래그 없이는 막는다. <code>/admin/uptime</code> 요약은 raw+rollup을 중복 없이 합산하고 상세 이벤트 목록은 최근 raw 샘플만 보여준다.</p>
<pclass="dg-note">31차 적용(2026-06-28): 운영 티켓 중복 처리는 자동 병합이 아니라 수동 연결 1차로 닫았다. 사용자 티켓 생성은 결정론적 <code>fingerprint</code>를 저장하고, <code>/admin/tickets</code>는 같은 fingerprint 후보 수·oldest parent 후보·하위 티켓 수를 반환한다. 관리자는 기존 <code>PATCH /admin/tickets/{id}</code>로 <code>parent_ticket_id</code>를 지정/해제하며, self-parent와 cycle은 서버가 거부한다. Settings의 사용자별 티켓 조회에는 fingerprint/parent를 노출하지 않는다.</p>
<pclass="dg-note">27차 적용(2026-06-28): 백엔드 언어 방향은 Node.js 우선 교체 가능성으로 정리하되 FastAPI 전면 재작성은 보류했다. 첫 경계로 <code>app/contracts/engine_gateway.py</code>를 추가해 <code>/v1/generate</code>, <code>/v1/stream</code>, SSE <code>token/done/error</code> 계약을 FastAPI client와 현재 Python gateway가 함께 쓰게 했다. 결정 기록은 <code>docs/decisions/backend-node-transition.md</code>에 둔다.</p>
<pclass="dg-note">27차 적용(2026-06-28): 백엔드 언어 방향은 Node.js 우선 교체 가능성으로 정리하되 FastAPI 전면 재작성은 보류했다. 첫 경계로 <code>app/contracts/engine_gateway.py</code>를 추가해 <code>/v1/generate</code>, <code>/v1/stream</code>, SSE <code>token/done/error</code> 계약을 FastAPI client와 현재 Python gateway가 함께 쓰게 했다. 결정 기록은 <code>docs/decisions/backend-node-transition.md</code>에 둔다.</p>
<pclass="dg-note">32차 적용(2026-06-28): Engine gateway SSE parsing을 <code>app/contracts/engine_gateway.py</code>의 <code>token/done/error</code> wire contract로 이동했다. gateway token payload, done telemetry, error detail 검증은 공유 계약 decoder가 맡고, gateway error는 저장 턴을 만들지 않는 기존 경계를 유지한다. 34차에서 이 raw line 해석 소유권은 <code>EngineClient.stream_packets()</code> 경계로 올려 Node.js gateway 교체를 위한 계약 증거를 강화했다.</p>
<pclass="dg-note">33차 적용(2026-06-28): H1 pre/post evidence 원장을 학습자 <code>SessionReview</code> 화면에 연결했다. 학습자는 자기효능감·기술숙련도·수련만족도 3척도의 사전/사후 1~5 aggregate score를 조회·저장할 수 있고, 화면 문구는 효과 판정·통계 검정이 평가설계 확정 이후 별도 산출임을 명시한다. 구현 중 React 이벤트 값을 상태 updater 안에서 늦게 참조해 앱이 검은 배경만 남는 크래시도 확인해 수정했다.</p>
<pclass="dg-note">34차 적용(2026-06-28): Node.js gateway 교체 가능성을 위해 엔진 스트림 wire parsing 소유권을 <code>EngineClient.stream_packets()</code>로 올렸다. 앱 orchestrator는 raw SSE line이 아니라 <code>EngineGatewaySsePacket</code>만 처리하고, Python gateway의 <code>/v1/generate</code>도 <code>GenerateResponse.model_dump()</code>로 반환해 hand-mirrored dict drift를 줄였다. focused gateway/orchestrator/session 회귀는 46 passed.</p>
<pclass="dg-note">35차 적용(2026-06-28): X2 비용 통제 후속으로 fast/deep evaluator structured 결과에 인메모리 semantic cache 1차를 추가했다. 캐시 키는 canonical evaluator request의 SHA-256이며 원문 prompt·completion은 저장하지 않는다. 성공 파싱 결과만 TTL/entry 제한 안에서 재사용하고, engine error·parse error는 캐시하지 않는다. focused evaluator/runtime/admin 회귀는 40 passed.</p>
<pclass="dg-note">36차 적용(2026-06-28): H1 pre/post 원장에서 Phase 3 evidence root용 <code>02-measures/prepost_measures.csv</code>와 <code>02-measures/kpi_report.json</code>을 생성하는 1차 계산기를 추가했다. participant id는 <code>P3-###</code>로 가명화하고, 3척도 paired normalized mean pre/post/delta, complete/missing pair를 report에 채운다. 공식 효과성 판정·통계검정·실험/통제군 배정은 아직 평가설계 게이트다. focused H1/backend 회귀는 45 passed.</p>
<pclass="dg-note">37차 적용(2026-06-28): H4 한국어 NAME/ORG 마스킹에 합성 fixture 기반 평가 harness를 추가했다. <code>data/privacy/pii-masking-ko-fixtures.json</code>의 5개 케이스를 <code>scripts/evaluate-pii-masking.py</code>로 평가하며, entity recall·forbidden substring removal·unexpected entity violation을 출력한다. fixture가 잡은 <code>소속</code>/<code>안내</code> NAME 오탐은 stopword로 보정했다. focused H4 회귀는 41 passed, CLI fixture 평가는 5/5 pass.</p>
<pclass="dg-note">38차 적용(2026-06-28): M2 다회기 케이스 아크 2차로 세션 종료 시 마스킹 축어록 기반 fallback <code>session_summary.digest</code>와 <code>case_profile.case_digest</code>, <code>rapport_trajectory</code>, <code>alliance_level</code>을 갱신한다. 다음 회기 seed recall은 <code>case_digest</code>·직전 요약·client-visible <code>pinned_fact</code>를 함께 조립한다. 당시 후속이던 <code>pinned_fact</code> 자동 쓰기는 43차에서 보수적 identity/agreement 범위로 연결했다. focused M2 회귀는 43 passed.</p>
<pclass="dg-note">39차 적용(2026-06-28): X2 evaluator semantic cache 운영 관측을 <code>/admin/usage</code>와 관리자 <code>AI 비용</code> 카드에 노출했다. 응답은 cache key나 prompt/completion 없이 enabled, entries, hits, misses, stores, evictions, requests, hit_rate만 반환한다. 프론트는 구버전 API 응답에도 fallback해 빈 화면 크래시를 막는다. focused X2/backend 40 passed, API typecheck/build, admin E2E 1 passed.</p>
<pclass="dg-note">40차 적용(2026-06-28): H3 페르소나 draft 생성 프롬프트에 <code>persona-draft-rag@2026-06-28.1</code> bundle pinning을 추가했다. <code>POST /personas/drafts/generate</code>는 engine metadata와 draft <code>source_provenance</code>에 prompt bundle id/version/hash를 남겨 source/chunk 증거와 생성 프롬프트 버전을 함께 추적한다. focused persona review 회귀는 31 passed.</p>
<pclass="dg-note">41차 적용(2026-06-28): X2 장기 비용 추이 1차로 <code>/admin/usage</code>에 일별 <code>daily_cost</code> bucket(day, turns, tokens_in/out, cost_usd)을 추가하고, 관리자 <code>AI 비용</code> 카드에 최근 일별 비용/턴 추이를 표시했다. DB와 dev runtime fallback 모두 같은 shape를 반환한다. 자동 차단·한도 enforcement는 운영 정책 결정이 필요해 후속으로 남긴다. backend 40 passed, API typecheck/build, admin E2E 1 passed.</p>
<pclass="dg-note">42차 적용(2026-06-28): M1 비언어 이벤트 2차로 voice learner turn에 <code>provider_events</code> JSONB 보존 슬롯을 추가했다. WebSocket control/STT provider 이벤트는 allowlist·개수·문자열 길이 제한을 거쳐 <code>app.turns.provider_events</code>에 저장되고, raw transcript/text payload는 보존하지 않는다. 당시 기준으로 review/public API에는 아직 노출하지 않고 기존 <code>nonverbal</code> 칩은 audio/silence/pace/barge-in 파생만 유지했다. focused voice/session 회귀는 44 passed.</p>
<pclass="dg-note">43차 적용(2026-06-28): M2 다회기 메모리 3차로 세션 종료 시 마스킹된 client-visible 발화에서 보수적 <code>pinned_fact</code> 후보를 자동 실적재한다. 1차 범위는 <code>[NAME]</code>/<code>[ORG]</code> identity와 명시적 상담 약속만이며, 관계갈등·위기·임상 추론은 pinning하지 않는다. learner-owned <code>case_profile</code> 기준 RLS insert/update 정책을 추가했고, 다음 회기 recall은 기존 client-visible <code>pinned_fact</code> 읽기 경로를 그대로 사용한다. focused M2 회귀는 60 passed.</p>
<pclass="dg-note">44차 적용(2026-06-28): M2 다회기 메모리 4차로 <code>pinned_fact</code> 삽입 또는 값 변경 시 <code>pinned_fact_history</code>에 append-only 이력을 남긴다. 같은 값 재확인은 history를 늘리지 않고, <code>locked</code> fact는 계속 건드리지 않는다. 이유값은 최초 기록 <code>progression</code>, 값 변경 <code>clarification</code>로 제한했으며, 자동 모순 판정은 아직 후속이다. focused M2 회귀는 62 passed.</p>
<pclass="dg-note">45차 적용(2026-06-28): M1 비언어 이벤트 3차로 저장되는 <code>provider_events</code>에 내부 taxonomy <code>event_type</code>/<code>category</code>를 붙인다. raw provider <code>type</code>/<code>kind</code>/<code>label</code>은 보존하되 sigh/cry/laugh/voice_activity/noise 등은 canonical key로 정규화한다. raw transcript/text payload는 계속 drop했고, 당시 기준으로 review/public API 노출은 하지 않았다. focused voice/session 회귀는 45 passed.</p>
<pclass="dg-note">46차 적용(2026-06-28): C3 이론모드 2차로 세션 시작 전 화면에 <code>humanistic</code>/<code>cbt</code>/<code>integrative</code> 명시 선택 segmented control을 추가했다. 기본값은 기존처럼 페르소나 <code>theory_target</code>에서 잡고, 사용자가 바꾼 값은 <code>POST /sessions</code>의 <code>theory_mode</code>로 전송된다. CBT 체인·임상 문안·이론부합 루브릭은 만들지 않았다. 검증: web typecheck/build, C3 backend focused 31 passed, session-layout desktop/mobile 8 passed, session-mvp 1 passed, layout visual gate 9 passed.</p>
<pclass="dg-note">47차 적용(2026-06-28): H3 seed/version materializer runner 1차로 <code>scripts/materialize-persona-seeds.py</code>를 추가했다. 기본은 DB를 건드리지 않는 dry-run manifest 보고이고, <code>--json</code>은 P1~P7 <code>seed_version</code>/<code>persona_id</code>/<code>source_provenance</code>를 기계 판독 형태로 내보내며, <code>--apply</code>일 때만 DB pool을 초기화한 뒤 기존 idempotent <code>materialize_seed_personas()</code>를 호출한다. 당시 후속이던 source pack <code>content_hash</code>/document version 증분 sync runner는 48차에서, 원문 격리 정책 1차는 55차에서 처리했다. 검증: runner py_compile, <code>py -3.11 scripts\materialize-persona-seeds.py --json</code> dry-run <code>manifest_count=7</code>, focused persona review 33 passed.</p>
<pclass="dg-note">48차 적용(2026-06-28): H3/H2 repo-managed source pack version sync runner 1차로 <code>app.services.source_pack_sync</code>와 <code>scripts/sync-persona-sources.py</code>를 추가했다. 기존 <code>POST /kb/live-coach/source-packs/sync</code>도 같은 service를 사용하며, active <code>kb.document</code>의 <code>content_hash</code>가 같으면 skip하고 다르면 <code>previous_version + 1</code>로 색인해 <code>UNIQUE(source_id, doc_uri, version)</code> 충돌을 피한다. dry-run은 DB 상태를 읽되 write하지 않고, <code>--apply</code>에서만 <code>kb.source</code> upsert와 <code>kb.document/kb.chunk</code> 색인을 수행한다. 당시 남은 원문 격리 정책 1차는 55차에서 처리했고, 임상팀 최종 검수 evidence는 여전히 후속이다. 검증: source sync py_compile, <code>py -3.11 scripts\sync-persona-sources.py --help</code>, <code>test_live_coach_sources.py</code> 4 passed.</p>
<pclass="dg-note">49차 적용(2026-06-28): M1 비언어 이벤트 4차로 인증된 회기 리뷰의 <code>nonverbal</code> 칩에 정규화된 provider taxonomy 일부를 제한 노출한다. <code>sigh</code>/<code>cry</code>/<code>laugh</code>/<code>breath</code>, prosody, background noise 같은 <code>event_type</code>은 한글 label/detail로 파생하고, provider/source/raw type/text/transcript는 응답에 싣지 않는다. 공개 공유 카드도 축어록과 provider raw를 계속 제외한다. 검증: M1 backend focused 45 passed, <code>npm run check:api-types</code>, <code>npm run typecheck</code>, <code>npm run build</code>, <code>session-review.spec.ts</code> 3 passed.</p>
<pclass="dg-note">50차 적용(2026-06-28): M2 pinned fact 모순 처리 1차로 명시적 상담 약속 철회/부정만 <code>agreement:counseling</code> 기존 fact의 <code>status='contradicted'</code>로 격리한다. 새 contradicted fact를 임의 생성하지 않고, non-locked 기존 fact가 있을 때만 업데이트하며 history reason은 <code>contradiction</code>으로 남긴다. 관계갈등·위기·임상 추론과 값 다름 일반 케이스는 계속 자동 모순 처리하지 않는다. 당시 검증: <code>test_session_memory.py</code> 11 passed, M2 focused 64 passed.</p>
<pclass="dg-note">51차 적용(2026-06-28): L1 Node.js 전환 계약 증거 2차로 <code>apps/api/engine_gateway/golden/engine_gateway_contract.v1.json</code> golden fixture와 <code>engine_gateway_schema.v1.json</code> schema artifact를 추가했다. fixture는 <code>/v1/generate</code> request/response, <code>/v1/stream</code> token/done/error frame과 decoded packet, provider <code>[DONE]</code> compatibility line을 고정한다. schema는 Node.js gateway가 Python import 없이 검증할 request/response/event shape를 고정한다. Python 계약 테스트가 schema drift와 fixture validation을 같이 보므로, 미래 Node.js gateway는 Python JSON whitespace가 아니라 같은 artifact의 의미론적 packet 동등성으로 conformance를 증명하면 된다. 당시 검증: gateway contract 18 passed, L1 focused 49 passed.</p>
<pclass="dg-note">52차 적용(2026-06-28): L1 Node.js 전환 계약 증거 3차로 <code>scripts/check-engine-gateway-contract.mjs</code> conformance runner를 추가했다. 이 runner는 Python import 없이 golden fixture와 schema artifact를 읽고, generate request/response shape, stream <code>token/done/error</code> frame decoding, provider <code>[DONE]</code> compatibility line 무시를 검증한다. <code>engine_gateway/test_gateway_model.py</code>도 Node runner를 subprocess로 호출해 Python 계약 테스트와 Node 계약 검증이 같은 artifact를 공유하도록 했다. 검증: <code>node scripts/check-engine-gateway-contract.mjs --json</code>, gateway contract 19 passed, L1 focused 50 passed.</p>
<pclass="dg-note">53차 적용(2026-06-28): M2 episodic embedding writer 1차로 세션 종료 저장 성공 뒤 <code>_write_episodic_embeddings()</code> background task를 예약한다. writer는 <code>speaker='client'</code>, <code>turn_id</code> 있음, <code>client</code> visible, <code>text_masked</code> nonempty 조건만 통과시키고 원문 <code>text</code>, 상담자 발화, evaluator-only turn, 평가 payload는 색인하지 않는다. <code>app.turn_embedding</code> 쓰기는 BGE-M3 dense/sparse를 <code>ON CONFLICT (turn_id) DO NOTHING</code>으로 idempotent 저장하며, DB/pgvector/embedder 미구성은 종료 저장을 실패시키지 않는다. 검증: <code>test_session_memory.py</code> 14 passed, M2 focused 67 passed.</p>
<pclass="dg-note">54차 적용(2026-06-28): X2 모델별 비용 리포트 1차로 <code>app.services.usage_report</code>와 <code>scripts/report-ai-usage.py</code>를 추가했다. 기존 <code>/admin/usage</code> JSON을 입력으로 provider/model별 cost share, token share, cost/turn, cost/1k tokens, metered coverage, budget/cache warning을 산출하되 자동 차단이나 한도 enforcement 정책은 만들지 않는다. 검증: usage report 2 tests, X2 focused 42 passed, CLI sample report schema <code>vignette.ai_usage_model_cost_report.v1</code> 생성.</p>
<pclass="dg-note">55차 적용(2026-06-28): H3 원문 격리본 정책 1차로 <code>kb.raw_source_artifact</code> hash-only 레코드를 추가하고, <code>POST /personas/sources</code>가 raw 원문 hash와 sanitized RAG source를 분리해 기록하게 했다. <code>rag.index_document()</code>는 <code>sensitivity=3</code> 또는 raw source marker 청크를 DB 접근 전에 <code>IndexPolicyViolation</code>으로 차단하므로 raw 원문은 <code>kb.chunk</code>/embedding/FTS로 들어가지 않는다. 암호화 blob/vault 저장은 후속. 검증: raw isolation py_compile, H3 focused 38 passed.</p>
<pclass="dg-note">56차 적용(2026-06-28): H2 회기 리뷰 2열 UI 1차로 학습자 <code>SessionReview</code> 데스크톱 레이아웃을 좌측 축어록 타임라인, 우측 요약·감정·흐름·루브릭·강점·개선점·pre/post·워크시트·피드백 작업열로 재배치했다. 교수자 검토 화면과 모바일/태블릿 반응형은 기존 규칙을 유지한다. 검증: web typecheck, <code>session-review.spec.ts</code> 3 passed, <code>layout-visual-gate.spec.ts</code> 9 passed, 1440 desktop screenshot 직접 확인.</p>
<pclass="dg-note">57차 적용(2026-06-28): L1 Node.js 전환 준비 4차로 브라우저-facing 세션 read-model을 <code>app/session_read_model.py</code>로 분리했다. <code>routes/sessions.py</code>는 route/auth/RLS DB read/persistence를 유지하고, 세션 목록·대시보드·상세·리뷰·공유 payload DTO와 deterministic builder는 read-model 모듈이 소유한다. OpenAPI field shape와 hidden-turn/evaluator-only filtering, saved worksheet 우선순위, teacher review read-only 경계는 유지했다. 검증: session read-model py_compile, focused backend 48 passed, <code>npm run check:api-types</code>, <code>npm run typecheck</code>.</p>
<pclass="dg-note">58차 적용(2026-06-28): 학습 세션 텍스트 턴은 학습자 발화 버블을 전송 즉시 표시하고, 내담자 응답 전에는 임시 대기 버블만 보여준다. 채팅 입력은 응답 생성 대기 중에만 막고, 음성 TTS 재생 중에는 자막이 먼저 남은 상태에서 텍스트 입력을 유지하며 <code>음성 건너뛰기</code> 버튼으로 재생을 중단할 수 있다. 검증: web typecheck, <code>session-mvp.spec.ts</code> 1 passed, <code>voice-success.spec.ts</code> synthetic UI 1 passed.</p>
<pclass="dg-note">59차 적용(2026-06-28): L1 Node.js 전환 준비 5차로 브라우저-facing 페르소나 DTO와 deterministic mapper를 <code>app/persona_read_model.py</code>로 분리했다. <code>routes/personas.py</code>는 route/auth/teacher-admin gate/DB repository/RAG source 등록/LLM draft generation/error mapping을 유지한다. <code>PersonaSummary</code>, <code>PersonaReviewSummary</code>, <code>PersonaDraftPayload</code>, <code>PersonaDraftDetail</code>, source/evidence 응답 schema 이름과 <code>source/degraded</code> fail-closed catalog 경계는 유지했다. 검증: persona read-model py_compile, <code>test_persona_review.py</code> 33 passed, <code>npm run check:api-types</code>, <code>npm run typecheck</code>.</p>
<pclass="dg-note">20차 적용(2026-06-28): 신규 Google/SAML 사용자는 <code>account_status=pending</code>으로 시작하고 승인 전에는 <code>/pending</code> 안내 화면만 본다. <code>yunchan@twentyoz.kr</code>는 슈퍼 관리자 allowlist로 admin+approved를 받으며, <code>/admin/users</code>는 가입 승인 탭에서 pending 계정을 승인 또는 보류 처리한다.</p>
<pclass="dg-note">20차 적용(2026-06-28): 신규 Google/SAML 사용자는 <code>account_status=pending</code>으로 시작하고 승인 전에는 <code>/pending</code> 안내 화면만 본다. <code>yunchan@twentyoz.kr</code>는 슈퍼 관리자 allowlist로 admin+approved를 받으며, <code>/admin/users</code>는 가입 승인 탭에서 pending 계정을 승인 또는 보류 처리한다.</p>
<pclass="dg-note">21차 적용(2026-06-28): 관리자 페이지 진입권을 기본 역할과 분리해 <code>app_user.admin_access</code>로 저장한다. <code>AUTH_SUPER_ADMIN_EMAILS</code> 기본값은 <code>yunchan@twentyoz.kr</code>, <code>hoonjungkoo@hs.ac.kr</code>이며, 슈퍼 관리자는 학습자·교수자·관리자 공간 전환과 관리자 권한 부여/회수를 할 수 있다. 학생·교수 계정도 <code>admin_access=true</code>면 우측 상단 관리자 진입이 노출된다. 구성 슈퍼 관리자의 권한 회수와 계정 비활성화는 차단한다.</p>
<pclass="dg-note">21차 적용(2026-06-28): 관리자 페이지 진입권을 기본 역할과 분리해 <code>app_user.admin_access</code>로 저장한다. <code>AUTH_SUPER_ADMIN_EMAILS</code> 기본값은 <code>yunchan@twentyoz.kr</code>, <code>hoonjungkoo@hs.ac.kr</code>이며, 슈퍼 관리자는 학습자·교수자·관리자 공간 전환과 관리자 권한 부여/회수를 할 수 있다. 학생·교수 계정도 <code>admin_access=true</code>면 우측 상단 관리자 진입이 노출된다. 구성 슈퍼 관리자의 권한 회수와 계정 비활성화는 차단한다.</p>
<pclass="dg-note">25차 적용(2026-06-28): <code>/admin/users</code>에서 허용 도메인 밖 이메일도 정확한 계정 단위로 강제 등록할 수 있다. Google/SAML/dev-login은 미리 등록된 이메일만 도메인 게이트 예외로 통과시키고, provider 로그인 시 기존 관리 row의 역할·코호트·승인 상태를 이어받는다. 미등록 외부 도메인 로그인은 계속 차단한다.</p>
<pclass="dg-note">25차 적용(2026-06-28): <code>/admin/users</code>에서 허용 도메인 밖 이메일도 정확한 계정 단위로 강제 등록할 수 있다. Google/SAML/dev-login은 미리 등록된 이메일만 도메인 게이트 예외로 통과시키고, provider 로그인 시 기존 관리 row의 역할·코호트·승인 상태를 이어받는다. 미등록 외부 도메인 로그인은 계속 차단한다.</p>
@ -491,8 +523,8 @@
<tr><td>상담 세션</td><td>회기 단계, 내담자 컨텍스트, 아바타, 음성 오브, 실시간 자막, 텍스트 폴백, 안전 점검</td><td>화면 중심은 내담자, 피드백은 조용한 신호</td></tr>
<tr><td>상담 세션</td><td>회기 단계, 내담자 컨텍스트, 아바타, 음성 오브, 실시간 자막, 텍스트 폴백, 안전 점검</td><td>화면 중심은 내담자, 피드백은 조용한 신호</td></tr>
<tr><td>회기 리뷰</td><td>요약, 감정 밸런스, 회기 흐름, 축어록, 기법 태그, 개선점, 오디오/PDF</td><td>점수 폭격이 아니라 근거 기반 회고</td></tr>
<tr><td>회기 리뷰</td><td>요약, 감정 밸런스, 회기 흐름, 축어록, 기법 태그, 개선점, 오디오/PDF</td><td>점수 폭격이 아니라 근거 기반 회고</td></tr>
<tr><td>교수 콘솔</td><td>리뷰 대기 큐, 코호트 현황, 페르소나 검수, 종료 회기 목록</td><td>awareness보다 triage에 가까운 교수자 작업대</td></tr>
<tr><td>교수 콘솔</td><td>리뷰 대기 큐, 코호트 현황, 페르소나 검수, 종료 회기 목록</td><td>awareness보다 triage에 가까운 교수자 작업대</td></tr>
<tr><td>관리자 콘솔</td><td>운영 홈: 교육 진행·온라인 사용자·헬스 샘플 이력·AI 비용·운영 티켓 요약. 하위 메뉴: 사용자 관리, 접근 권한, 운영 티켓 처리 큐</td><td>상태 관측과 관리 작업을 분리한 실제 운영 콘솔</td></tr>
<tr><td>관리자 콘솔</td><td>운영 홈: 교육 진행·온라인 사용자·헬스 샘플 이력·AI 비용·운영 티켓 요약. 하위 메뉴: 사용자 관리, 접근 권한, 운영 티켓 처리 큐. 헬스 샘플은 화면 조회와 synthetic sampler가 같은 DB 이력에 남김</td><td>상태 관측과 관리 작업을 분리한 실제 운영 콘솔</td></tr>
<tr><td>설정</td><td>계정, 관리자 AI 운영 설정, 테마, 음성 프리셋, 속도, 알림</td><td>역할별 설정만 보이는 조용한 환경 관리</td></tr>
<tr><td>설정</td><td>계정, 관리자 AI 운영 설정, 지원 요청 현황, 테마, 음성 프리셋, 속도, 알림</td><td>역할별 설정과 본인 지원 요청 상태만 보이는 조용한 환경 관리</td></tr>
<imgsrc="design-concepts/generated/06-admin-responsive.png"alt="관리자 콘솔 PC 태블릿 모바일 반응형 디자인 시안"loading="lazy">
<imgsrc="design-concepts/generated/06-admin-responsive.png"alt="관리자 콘솔 PC 태블릿 모바일 반응형 디자인 시안"loading="lazy">
<divclass="dg-card-body"><b>관리자 콘솔</b><span>운영 홈은 서비스 상태 관측에 집중하고 사용자·권한·티켓은 별도 메뉴로 분리. 티켓과 헬스 샘플은 DB 기반 1차 모델을 사용.</span><code>06-admin-responsive.png</code></div>
<divclass="dg-card-body"><b>관리자 콘솔</b><span>운영 홈은 서비스 상태 관측에 집중하고 사용자·권한·티켓은 별도 메뉴로 분리. 티켓과 헬스 샘플은 DB 기반 모델을 사용하고 sampler가 주기 적재 가능.</span><code>06-admin-responsive.png</code></div>
</article>
</article>
<articleclass="dg-card">
<articleclass="dg-card">
<imgsrc="design-concepts/generated/07-settings-responsive.png"alt="설정 화면 PC 태블릿 모바일 반응형 디자인 시안"loading="lazy">
<imgsrc="design-concepts/generated/07-settings-responsive.png"alt="설정 화면 PC 태블릿 모바일 반응형 디자인 시안"loading="lazy">
<divclass="dg-card-body"><b>설정</b><span>계정, AI 운영, 테마, 음성, 알림을 역할별로 노출하는 차분한 설정 화면.</span><code>07-settings-responsive.png</code></div>
<divclass="dg-card-body"><b>설정</b><span>계정, AI 운영, 지원 요청, 테마, 음성, 알림을 역할별로 노출하는 차분한 설정 화면.</span><code>07-settings-responsive.png</code></div>
</article>
</article>
<articleclass="dg-card">
<articleclass="dg-card">
<imgsrc="design-concepts/generated/08-warm-visual-elements.png"alt="온화한 배경과 패턴 이미지 요소 보드"loading="lazy">
<imgsrc="design-concepts/generated/08-warm-visual-elements.png"alt="온화한 배경과 패턴 이미지 요소 보드"loading="lazy">
<buttonclass="scard-head"aria-expanded="false"><spanclass="chip c-done">개인화 1차 DONE</span><spanclass="scard-mid"><spanclass="scard-title">학습자 홈 대시보드 / 최근 회기 리캡</span><spanclass="scard-sum"><code>GET /sessions/dashboard</code>와 <code>LearnerHome</code>가 실제 세션 기반 성장·피드백·페르소나 진행을 표시.</span></span><spanclass="caret"aria-hidden="true"></span></button>
<buttonclass="scard-head"aria-expanded="false"><spanclass="chip c-done">개인화 1차 DONE</span><spanclass="scard-mid"><spanclass="scard-title">학습자 홈 대시보드 / 최근 회기 리캡</span><spanclass="scard-sum"><code>GET /sessions/dashboard</code>와 <code>LearnerHome</code>가 실제 세션 기반 성장·피드백·페르소나 진행을 표시.</span></span><spanclass="caret"aria-hidden="true"></span></button>
<divclass="scard-body"><divclass="kv"><b>현재</b><p>교수자 성장 집계와 중복되던 점수·라포·기법 계산을 <code>app.services.session_metrics</code>로 공용화했다. <code>GET /sessions/dashboard</code>는 학습자 본인 세션만 집계해 <code>overview</code>, <code>growth</code>, <code>persona_progress</code>, <code>achievements</code>, <code>recent_feedback</code>를 반환하고, <code>LearnerHome</code>은 누적 회기·리뷰 대기·최근 평가·라포 흐름·페르소나별 진행·최근 피드백·마지막 세션 리캡을 실제 서버 데이터로 보여준다. 성취는 공식 수료/등급이 아니라 실제 연습 milestone만 사용한다.</p></div><divclass="kv"><b>검증</b><p><code>pytest app/test_learner_dashboard.py app/test_teacher_dashboard.py app/test_rbac_idor.py -q</code> 12 passed, <code>npm run check:api-types</code>, <code>npm run typecheck</code>, <code>npm run build</code>, <code>learner.spec.ts</code> 6 passed, <code>session-review.spec.ts</code> 2 passed, <code>layout-visual-gate.spec.ts</code>7 passed, <code>session-layout.spec.ts</code> 4 passed.</p></div><divclass="kv k-warn"><b>잔여</b><p>UX 감사 기준으로 2차에서는 카드 격자 비중을 줄이고 게이지·small multiples·테이블 중심으로 정리해야 한다. 모바일에서 임상 요약을 단순 숨김 처리하는 부분도 접힘/요약으로 바꾸는 게 맞다.</p></div></div>
<divclass="scard-body"><divclass="kv"><b>현재</b><p>교수자 성장 집계와 중복되던 점수·라포·기법 계산을 <code>app.services.session_metrics</code>로 공용화했다. <code>GET /sessions/dashboard</code>는 학습자 본인 세션만 집계해 <code>overview</code>, <code>growth</code>, <code>persona_progress</code>, <code>achievements</code>, <code>recent_feedback</code>를 반환하고, <code>LearnerHome</code>은 누적 회기·리뷰 대기·최근 평가·라포 흐름·페르소나별 진행·최근 피드백·마지막 세션 리캡을 실제 서버 데이터로 보여준다. 성취는 공식 수료/등급이 아니라 실제 연습 milestone만 사용한다.</p></div><divclass="kv"><b>검증</b><p><code>pytest app/test_learner_dashboard.py app/test_teacher_dashboard.py app/test_rbac_idor.py -q</code> 12 passed, <code>npm run check:api-types</code>, <code>npm run typecheck</code>, <code>npm run build</code>, <code>learner.spec.ts</code> 6 passed, <code>session-review.spec.ts</code> 2 passed, <code>layout-visual-gate.spec.ts</code>9 passed, <code>session-layout.spec.ts</code> 4 passed.</p></div><divclass="kv k-warn"><b>잔여</b><p>UX 감사 기준으로 2차에서는 카드 격자 비중을 줄이고 게이지·small multiples·테이블 중심으로 정리해야 한다. 모바일에서 임상 요약을 단순 숨김 처리하는 부분도 접힘/요약으로 바꾸는 게 맞다.</p></div></div>
<buttonclass="scard-head"aria-expanded="false"><spanclass="chip c-plan">중기</span><spanclass="scard-mid"><spanclass="scard-title">2-tier 모델 전략 + Node 교체 가능성<spanclass="ownbadge">내 차례</span></span><spanclass="scard-sum">2-tier는 ENGINE_MODE로 점진 도입 가능. 백엔드는 Node.js 우선 교체 가능성 방향으로 결정.</span></span><spanclass="caret"aria-hidden="true"></span></button>
<buttonclass="scard-head"aria-expanded="false"><spanclass="chip c-plan">중기</span><spanclass="scard-mid"><spanclass="scard-title">2-tier 모델 전략 + Node 교체 가능성<spanclass="ownbadge">내 차례</span></span><spanclass="scard-sum">2-tier는 ENGINE_MODE로 점진 도입 가능. 백엔드는 Node.js 우선 교체 가능성 방향으로 결정.</span></span><spanclass="caret"aria-hidden="true"></span></button>
<divclass="scard-body"><divclass="kv"><b>현재</b><p>① PDF: 고성능(Claude·GPT=내담자발화·실시간평가)/저비용 한국어(Solar=단순응답·사후요약) 2단계 + 캐싱 90%절감. 현재 client AI는 claude_cli 기본 라우팅, fast/deep evaluator는 <code>EVALUATOR_FAST_MODEL</code>/<code>EVALUATOR_DEEP_MODEL</code> 설정 시 모델 override를 gateway에 전달한다. 벤더중립 인터페이스와 L0~L2 cache hint/session reuse는 있음. ② 백엔드 방향은 <b>Node.js 우선 교체 가능성</b>으로 정했다. 단, 현재 <b>Python/FastAPI</b>는 납품 핵심 경로라 전면 재작성하지 않고, 계약 경계를 먼저 고정해 갈아치울 수 있게 만든다.</p></div><divclass="kv k-warn"><b>권고</b><p>2-tier는 evaluator 모델 override 이후 client AI/요약 모델 정책과 semantic cache로 점진 도입. Node 전환은<code>engine_gateway</code>, persona DTO/mapper, session read-model처럼 런타임 경계가 명확한 부분부터 strangler 방식으로 진행한다.</p></div></div>
<divclass="scard-body"><divclass="kv"><b>현재</b><p>① PDF: 고성능(Claude·GPT=내담자발화·실시간평가)/저비용 한국어(Solar=단순응답·사후요약) 2단계 + 캐싱 90%절감. 현재 client AI는 claude_cli 기본 라우팅, fast/deep evaluator는 <code>EVALUATOR_FAST_MODEL</code>/<code>EVALUATOR_DEEP_MODEL</code> 설정 시 모델 override를 gateway에 전달한다. evaluator structured 결과는 canonical request SHA-256 기반 인메모리 semantic cache 1차로 중복 호출을 줄이며, 원문 prompt·completion은 캐시에 저장하지 않는다. 벤더중립 인터페이스와 L0~L2 cache hint/session reuse도 유지한다. ② 백엔드 방향은 <b>Node.js 우선 교체 가능성</b>으로 정했다. 단, 현재 <b>Python/FastAPI</b>는 납품 핵심 경로라 전면 재작성하지 않고, 계약 경계를 먼저 고정해 갈아치울 수 있게 만든다. 이번 패스에서 앱 orchestrator가 raw gateway SSE line을 직접 해석하지 않도록 <code>EngineClient.stream_packets()</code> 경계를 추가했고, Python gateway의 <code>/v1/generate</code> 응답은 <code>GenerateResponse</code> 계약 모델로 반환한다. 추가로 <code>app/session_read_model.py</code>와 <code>app/persona_read_model.py</code>가 세션·페르소나 브라우저 응답 DTO와 builder/mapper를 소유하게 해, Node read API가 따라야 할 browser-facing contract 경계를 분리했다.</p></div><divclass="kv k-warn"><b>권고</b><p>2-tier 후속은 client AI/요약 모델 정책, evaluator cache의 운영 hit-rate 관측, 장기 한도 정책으로 점진 도입한다. Node 전환은 이미 고정한<code>engine_gateway</code>, <code>session_read_model</code>, <code>persona_read_model</code> 계약을 유지한다. live Node gateway endpoint conformance는 실제 Node gateway 구현이 생긴 뒤 붙이고, admin mapper cleanup은 RBAC/audit/RLS blast radius 때문에 뒤로 둔다.</p></div></div>
<divclass="scard-body"><divclass="kv"><b>현재</b><p>prepare_turn→state_machine.evolve에 crisis.risk_level을 ideation_observed로 전달(위기 관측 상태머신 반영). escalate=True 시 REST·SSE·voice가 엔진 호출 전 중단하고 <code>app.safety_events</code>에 109 resource와 <code>conversation_stopped</code>를 적재한다. 교수자 대시보드는 이 레코드를 안전 알림 큐로 표시한다. 안전 이벤트 insert payload는 <code>test_record_safety_event_writes_teacher_alert_payload</code>로 회귀화했다.</p></div><divclass="kv k-warn"><b>잔여(임상팀)</b><p>비밀보장 예외고지·단계적 탐색·생명유지서약 스크립트 콘텐츠 + 실시간 교수자 push/메일 + 위기탐색 누락 회기리뷰 감점.</p></div></div>
<divclass="scard-body"><divclass="kv"><b>현재</b><p>prepare_turn→state_machine.evolve에 crisis.risk_level을 ideation_observed로 전달(위기 관측 상태머신 반영). escalate=True 시 REST·SSE·voice가 엔진 호출 전 중단하고 <code>app.safety_events</code>에 109 resource와 <code>conversation_stopped</code>를 적재한다. 교수자 대시보드는 이 레코드를 안전 알림 큐로 표시한다. 안전 이벤트 insert payload는 <code>test_record_safety_event_writes_teacher_alert_payload</code>로 회귀화했다.</p></div><divclass="kv k-warn"><b>잔여(임상팀)</b><p>비밀보장 예외고지·단계적 탐색·생명유지서약 스크립트 콘텐츠 + 실시간 교수자 push/메일 + 위기탐색 누락 회기리뷰 감점.</p></div></div>
<buttonclass="scard-head"aria-expanded="false"><spanclass="chip c-done">C3 구조 DONE</span><spanclass="scard-mid"><spanclass="scard-title">이론모드 스레딩(humanistic/CBT)</span><spanclass="scard-sum">TurnContext+prepare_turn+build_turn_messages에 theory_mode 배선, 프론트 하드코딩 제거.</span></span><spanclass="caret"aria-hidden="true"></span></button>
<buttonclass="scard-head"aria-expanded="false"><spanclass="chip c-done">C3 2차 DONE</span><spanclass="scard-mid"><spanclass="scard-title">이론모드 스레딩 + 시작 전 명시 선택 UI</span><spanclass="scard-sum">theory_mode backend 배선, persona 기본값, humanistic/CBT/integrative segmented 선택과 POST 전송 검증.</span></span><spanclass="caret"aria-hidden="true"></span></button>
<divclass="scard-body"><divclass="kv"><b>현재</b><p>TurnContext+prepare_turn에 theory_mode, sessions/voice가 sess.theory_mode 전달, evaluator._theory_mode가 세션 이론(학습자 선택) 우선. <code>build_turn_messages</code>도 <code>THEORY_MODE_GUIDANCE</code>를 통해 인간중심·CBT·통합 이론 프레이밍을 엔진 메시지에 넣는다. 프론트 Session.tsx 'humanistic' 하드코딩 제거→persona.theory_target 기준(CBT 페르소나는 cbt). 회귀: <code>test_prepare_turn_threads_theory_mode_into_engine_messages</code> 포함 focused 20 passed.</p></div><divclass="kv k-warn"><b>잔여(임상팀)</b><p>CBT 이론별 단계 프롬프트 체인 콘텐츠 + 이론부합 채점 루브릭 + 프론트 이론 선택 UI(현재 persona 설계 이론 자동).</p></div></div>
<divclass="scard-body"><divclass="kv"><b>현재</b><p>TurnContext+prepare_turn에 theory_mode, sessions/voice가 sess.theory_mode 전달, evaluator._theory_mode가 세션 이론(학습자 선택) 우선. <code>build_turn_messages</code>도 <code>THEORY_MODE_GUIDANCE</code>를 통해 인간중심·CBT·통합 이론 프레이밍을 엔진 메시지에 넣는다. 프론트 <code>Session.tsx</code>는 기본값을 <code>persona.theory_target</code>에서 잡되, 세션 시작 전 <code>humanistic</code>/<code>cbt</code>/<code>integrative</code> segmented control로 학습자가 명시 선택할 수 있다. 선택값은 <code>POST /sessions</code>의 <code>theory_mode</code>로 전송된다. 검증: <code>npm run typecheck</code>, <code>npm run build</code>, C3 backend focused 31 passed, <code>session-layout.spec.ts</code> desktop/mobile 8 passed, <code>session-mvp.spec.ts</code> single-run 1 passed, <code>layout-visual-gate.spec.ts</code> 9 passed.</p></div><divclass="kv k-warn"><b>잔여(임상팀)</b><p>CBT 이론별 단계 프롬프트 체인 콘텐츠 + 이론부합 채점 루브릭은 후속이다. 이번 UI는 임상 문안·루브릭·백엔드 enum을 확장하지 않았다.</p></div></div>
<buttonclass="scard-head"aria-expanded="false"><spanclass="chip c-done">H4 부분 DONE</span><spanclass="scard-mid"><spanclass="scard-title">PII 마스킹 + LLM 호출 감사 + 프로필 온보딩·동의 게이트</span><spanclass="scard-sum">metadata-only llm_call_log, 단순 가입정보 온보딩, 약관 버전, 전역 URL 하드게이트. 전체 E2E 113.</span></span><spanclass="caret"aria-hidden="true"></span></button>
<buttonclass="scard-head"aria-expanded="false"><spanclass="chip c-done">H4 부분 DONE</span><spanclass="scard-mid"><spanclass="scard-title">PII 마스킹 + LLM 호출 감사 + 프로필 온보딩·동의 게이트</span><spanclass="scard-sum">metadata-only llm_call_log, 한국어 NAME/ORG 로컬 마스킹과 fixture 평가 harness, 전역 URL 하드게이트.</span></span><spanclass="caret"aria-hidden="true"></span></button>
<divclass="scard-body"><divclass="kv"><b>현재</b><p>정규식 폴백에 한국어 고위험 패턴 추가: 구체 날짜(2001.4.18→[DATE])·금액(1200원→[MONEY])·행정구역 주소(서울시 강남구 역삼동→[ADDR]). 상담 생성(generate/stream)·fast/deep 평가의 외부 LLM 호출 직후 <code>audit.llm_call_log</code>에 provider/model/token/cost/inference_geo/latency만 적재한다. prompt/completion 본문은 저장하지 않는다. 로컬 dev-login 실제 <code>/turn</code> smoke에서 <code>audit.llm_call_log</code>가 8→11로 3행 증가했다. 이번 패스에서 <code>app_user</code>에 이름·소속·학과·학년/직위·연락처·주소/수령지·닉네임·자기소개·아바타 URL·약관/개인정보 동의 시각·버전을 추가했다. 로그인 후 <code>onboarding_completed_at</code>이 없으면 전역 <code>OnboardingGate</code>가 <code>/onboarding</code> 외 URL(<code>/learn</code>, <code>/settings</code>, <code>/admin</code>, <code>/dev/avatar-preview</code>, <code>/login</code> 포함)을 모두 온보딩으로 돌리고, 온보딩 중에는 공용 셸 메뉴 없이 가입 직후 사용자 정보 입력 폼만 보여준다. learner 회기 시작도 온보딩 완료 전에는 막는다. <code>/users/me/avatar</code>는 PNG/JPG/WebP MIME·시그니처·3MB 제한 후 파일을 <code>USER_UPLOAD_DIR/profile-avatars</code>에 저장하고 URL만 보관한다. 약관·개인정보 처리방침 초안은 <code>/users/legal-docs</code>에서 제공한다. 관리자/교수자 권한은 온보딩에서 받지 않고 allowlist/관리자 사용자 관리로만 부여한다. 검증: <code>pytest app/ -q</code> 178 passed, <code>pytest engine_gateway/ -q</code> 11 passed, <code>npm run check:api-types</code>, <code>npm run typecheck</code>, <code>npm run build</code>, auth E2E 7 passed, 최종 <code>PLAYWRIGHT_PORT=5174 npm run e2e</code> 113 passed.</p></div><divclass="kv k-warn"><b>잔여</b><p>이름/기관명은 NER 필요(Presidio ko 모델·spaCy ko, 무거움 — TODO 명시). 미성년/guardian 및 법무 검토가 필요한 최종 서명 동의서·개인정보 처리방침·약관 evidence는 외부 게이트다. 공개 Google OAuth 실제 <code>/turn</code> proof는 별도 운영 게이트.</p></div></div>
<divclass="scard-body"><divclass="kv"><b>현재</b><p>정규식 폴백에 한국어 고위험 패턴 추가: 구체 날짜(2001.4.18→[DATE])·금액(1200원→[MONEY])·행정구역 주소(서울시 강남구 역삼동→[ADDR])에 더해, 이름/성명 라벨·성씨+이름+조사/호칭·대학교/학과/병원/센터 등 suffix 기반 한국어 NAME/ORG 로컬 휴리스틱 1차를 추가했다. 이번 패스에서 <code>app.services.pii_masking_eval</code>, <code>data/privacy/pii-masking-ko-fixtures.json</code>, <code>scripts/evaluate-pii-masking.py</code>를 추가해 합성 NAME/ORG fixture 5개를 entity recall·forbidden substring removal·unexpected entity violation으로 평가한다. fixture가 잡은 <code>소속</code>/<code>안내</code> NAME 오탐도 stopword로 보정했다. Presidio가 설치돼도 한국어 누락을 막기 위해 regex fallback을 후단에 한 번 더 태운다. 상담 생성(generate/stream)·fast/deep 평가의 외부 LLM 호출 직후 <code>audit.llm_call_log</code>에 provider/model/token/cost/inference_geo/latency만 적재한다. prompt/completion 본문은 저장하지 않는다. fast evaluator prompt의 내담자 응답과 DB에 저장되는 client turn <code>text_masked</code>도 마스킹본을 쓰게 보강했다. 로컬 dev-login 실제 <code>/turn</code> smoke에서 <code>audit.llm_call_log</code>가 8→11로 3행 증가했다. 이번 패스에서 <code>app_user</code>에 이름·소속·학과·학년/직위·연락처·주소/수령지·닉네임·자기소개·아바타 URL·약관/개인정보 동의 시각·버전을 추가했다. 로그인 후 <code>onboarding_completed_at</code>이 없으면 전역 <code>OnboardingGate</code>가 <code>/onboarding</code> 외 URL(<code>/learn</code>, <code>/settings</code>, <code>/admin</code>, <code>/dev/avatar-preview</code>, <code>/login</code> 포함)을 모두 온보딩으로 돌리고, 온보딩 중에는 공용 셸 메뉴 없이 가입 직후 사용자 정보 입력 폼만 보여준다. learner 회기 시작도 온보딩 완료 전에는 막는다. <code>/users/me/avatar</code>는 PNG/JPG/WebP MIME·시그니처·3MB 제한 후 파일을 <code>USER_UPLOAD_DIR/profile-avatars</code>에 저장하고 URL만 보관한다. 약관·개인정보 처리방침 초안은 <code>/users/legal-docs</code>에서 제공한다. 관리자/교수자 권한은 온보딩에서 받지 않고 allowlist/관리자 사용자 관리로만 부여한다. 검증: <code>pytest app/test_pii_masking_eval.py app/test_orchestrator_masking.py app/test_evaluation_persistence.py app/test_session_turn_persistence.py -q</code> 41 passed, <code>python scripts/evaluate-pii-masking.py --json</code> 5/5 pass, 과거 온보딩/동의 검증은 <code>pytest app/ -q</code> 178 passed, <code>pytest engine_gateway/ -q</code> 11 passed, <code>npm run check:api-types</code>, <code>npm run typecheck</code>, <code>npm run build</code>, auth E2E 7 passed, 최종 <code>PLAYWRIGHT_PORT=5174 npm run e2e</code> 113 passed.</p></div><divclass="kv k-warn"><b>잔여</b><p>한국어 이름/기관은 로컬 휴리스틱과 합성 fixture 평가 harness 1차까지 완료했지만, Presidio ko/spaCy ko/의료·학교 도메인 NER 기반 정밀화, 실제 운영 말뭉치 기반 오탐/미탐 평가, 미성년/guardian 및 법무 검토가 필요한 최종 서명 동의서·개인정보 처리방침·약관 evidence는 외부/후속 게이트다. 공개 Google OAuth 실제 <code>/turn</code> proof는 별도 운영 게이트.</p></div></div>
<buttonclass="scard-head"aria-expanded="false"><spanclass="chip c-plan">TRACK X1·X2 1차·L1</span><spanclass="scard-mid"><spanclass="scard-title">재귀학습 export(X1)·API 비용 관측(X2)·스택 정합성·일정(L1)<spanclass="ownbadge">내 차례</span></span><spanclass="scard-sum">X1/X2 1차와 evaluator 모델 override는 구현 완료. L1은 Node 방향 결정과 gateway 계약 1차 완료, 등재 문서 반영은 외부 gate.</span></span><spanclass="caret"aria-hidden="true"></span></button>
<buttonclass="scard-head"aria-expanded="false"><spanclass="chip c-plan">TRACK X1·X2 2차·L1</span><spanclass="scard-mid"><spanclass="scard-title">재귀학습 export(X1)·API 비용 관측(X2)·스택 정합성·일정(L1)<spanclass="ownbadge">내 차례</span></span><spanclass="scard-sum">X1 dry-run export, X2 비용 관측·모델 override·evaluator semantic cache·모델별 비용 리포트 완료. L1은 Node 방향 결정, gateway 계약, schema-backed golden fixture, Node conformance runner, session read-model 분리 완료. 등재 문서 반영은 외부 gate.</span></span><spanclass="caret"aria-hidden="true"></span></button>
<divclass="scard-body"><divclass="kv"><b>현재</b><p>X1: <code>scripts/export-recursive-dataset.py</code>와 <code>app.services.dataset_export</code>로 masked-text JSONL dry-run, PII scan, κ/ICC 계산, approved export 게이트를 구현했다. 기본은 <code>technical_dry_run</code>이고 <code>ds.*</code> write는 <code>--write-dataset</code> 명시 시에만 수행한다. X2: <code>app.turns</code>의 provider/model/tokens/cost를 최근 7일 기준으로 집계하는 <code>GET /admin/usage</code>를 추가했고, 운영 콘솔 <code>/admin</code>에 누적 비용·입출력 토큰·계량 커버리지·provider/model breakdown과<code>ADMIN_USAGE_BUDGET_USD</code> 기반 예산 경고(ok/warn/exceeded)를 표시한다. <code>EVALUATOR_FAST_MODEL</code>/<code>EVALUATOR_DEEP_MODEL</code> 설정 시 fast/deep 평가 호출만 해당 모델 override로 gateway에 전달하고, 비워두면 기존 default 라우팅을 유지한다. DB 미가용 dev에서는 runtime store로 fallback하지만 prod에서는 fail-closed한다. L1: 소유자 결정으로 Node.js 우선 교체 가능성 방향을 채택했다. <code>docs/decisions/backend-node-transition.md</code>에 FastAPI 유지 사유와 strangler 전환 원칙을 기록했고, <code>app/contracts/engine_gateway.py</code>를 추가해 Python client와 현재 gateway가 같은 요청/응답/SSE 계약을 공유한다.</p></div><divclass="kv k-warn"><b>권고</b><p>X1 approved export·골든셋 승격은 steward/legal 승인, reviewer disposition, IAA 게이트(κ≥0.6/ICC≥0.75) 이후만 진행한다. X2 후속은 semantic cache·장기 비용 추이/한도 정책·운영 모델별 비용 검증이다. L1의 남은 게이트는 신청서/저작권 등재 문서에 FastAPI 유지 사유와 Node 전환 계획을 반영하는 외부 거버넌스 증거다.</p></div></div>
<divclass="scard-body"><divclass="kv"><b>현재</b><p>X1: <code>scripts/export-recursive-dataset.py</code>와 <code>app.services.dataset_export</code>로 masked-text JSONL dry-run, PII scan, κ/ICC 계산, approved export 게이트를 구현했다. 기본은 <code>technical_dry_run</code>이고 <code>ds.*</code> write는 <code>--write-dataset</code> 명시 시에만 수행한다. X2: <code>app.turns</code>의 provider/model/tokens/cost를 최근 7일 기준으로 집계하는 <code>GET /admin/usage</code>를 추가했고, 운영 콘솔 <code>/admin</code>에 누적 비용·입출력 토큰·계량 커버리지·provider/model breakdown, 일별 <code>daily_cost</code> 추이,<code>ADMIN_USAGE_BUDGET_USD</code> 기반 예산 경고(ok/warn/exceeded)를 표시한다. <code>EVALUATOR_FAST_MODEL</code>/<code>EVALUATOR_DEEP_MODEL</code> 설정 시 fast/deep 평가 호출만 해당 모델 override로 gateway에 전달하고, 비워두면 기존 default 라우팅을 유지한다. fast/deep evaluator structured 결과는 canonical request SHA-256 키의 인메모리 semantic cache로 재사용한다. 캐시는 <code>EVALUATOR_SEMANTIC_CACHE_ENABLED</code>, <code>EVALUATOR_SEMANTIC_CACHE_TTL_SECONDS</code>, <code>EVALUATOR_SEMANTIC_CACHE_MAX_ENTRIES</code>로 제한하며, 원문 prompt·completion은 저장하지 않고 성공 파싱 결과만 캐시한다. <code>/admin/usage</code>는 evaluator cache enabled/entries/hits/misses/stores/evictions/requests/hit_rate도 함께 반환하고, 관리자 <code>AI 비용</code> 카드가 hit-rate와 일별 비용/턴 추이를 표시한다. <code>app.services.usage_report</code>와 <code>scripts/report-ai-usage.py</code>는 같은 usage JSON에서 provider/model별 cost share, token share, cost/turn, cost/1k tokens, budget/cache warning을 산출한다. DB 미가용 dev에서는 runtime store로 fallback하지만 prod에서는 fail-closed한다. L1: 소유자 결정으로 Node.js 우선 교체 가능성 방향을 채택했다. <code>docs/decisions/backend-node-transition.md</code>에 FastAPI 유지 사유와 strangler 전환 원칙을 기록했고, <code>app/contracts/engine_gateway.py</code>를 추가해 Python client와 현재 gateway가 같은 요청/응답/SSE 계약을 공유한다. 이어서 <code>EngineClient.stream_packets()</code>와 gateway <code>GenerateResponse</code> 반환으로 서비스 레이어의 wire-format 의존을 줄였다. <code>engine_gateway_contract.v1.json</code> golden fixture와 <code>engine_gateway_schema.v1.json</code> schema artifact로 generate request/response와 stream token/done/error packet을 고정했고, <code>scripts/check-engine-gateway-contract.mjs</code>가 같은 artifact를 Node.js에서 Python import 없이 검증한다. 이번 패스에서는 <code>app/session_read_model.py</code>를 추가해 세션 목록·대시보드·상세·리뷰·공유 payload의 DTO와 builder를 route에서 분리했고, <code>routes/sessions.py</code>는 auth/RLS DB read/persistence만 남기도록 좁혔다.</p></div><divclass="kv k-warn"><b>권고</b><p>X1 approved export·골든셋 승격은 steward/legal 승인, reviewer disposition, IAA 게이트(κ≥0.6/ICC≥0.75) 이후만 진행한다. X2 후속은 자동 차단·한도 enforcement 정책이다. L1의 남은 게이트는 신청서/저작권 등재 문서에 FastAPI 유지 사유와 Node 전환 계획을 반영하는 외부 거버넌스 증거다. 다음 내부 후보는 persona DTO/mapper 분리다.</p></div></div>
</article>
</article>
</div>
</div>
<divclass="detail-blocks">
<divclass="detail-blocks">
<divclass="detail-block"><h4>즉시 착수 가능(내부 코드, 외부 합의 불요)</h4><ul><li>C1은 저장형 워크시트까지 2차 완료. C2/C3/H2/H3은 1차 구조 완료. 후속은 임상 스크립트, CBT 체인, 2열 리뷰 UI, 페르소나 항목형 저작 UI처럼 데이터·정책·UI 고도화 영역이다.</li><li>H4(부분): 한국어 정규식 마스킹, metadata-only LLM call audit 경로, 로컬 live row proof, learner 동의 수락/철회/회기 시작 하드게이트 골격은 완료. 남은 내부 후보는 이름/기관 NER이고, guardian/legal 서명 동의 evidence는 외부 게이트다. X1 dry-run export 1차와 X2 비용 관측·예산 경고/evaluator 모델 override 1차는 완료, X2 semantic cache는 후속. M1/M2/M3 1차는 완료, provider 기반 한숨·울음 감지와 case_digest/pinned_fact 실적재, 운영 SAML 서명검증은 후속.</li></ul></div>
<divclass="detail-block"><h4>즉시 착수 가능(내부 코드, 외부 합의 불요)</h4><ul><li>C1은 저장형 워크시트까지 2차 완료. C2는 1차 구조 완료, C3은 이론모드 배선과 명시 선택 UI까지 2차 완료, H2는 라이브 코칭 1차와 source pack version sync 및 학습자 리뷰 2열 UI 1차, H3은 페르소나 저작 CRUD/RAG 생성/seed materializer runner/source pack sync runner/raw source hash-only isolation까지 완료. 후속은 임상 스크립트, CBT 체인, 페르소나 항목형 저작 UI처럼 데이터·정책·UI 고도화 영역이다.</li><li>H4(부분): 한국어 날짜/금액/주소 및 이름/기관 로컬 휴리스틱 마스킹, 합성 fixture 평가 harness, metadata-only LLM call audit 경로, 로컬 live row proof, learner 동의 수락/철회/회기 시작 하드게이트 골격은 완료. 남은 내부 후보는 실제 운영 말뭉치 기반 오탐/미탐 평가와 모델 기반 정밀 ko NER adapter 검토이고, guardian/legal 서명 동의 evidence는 외부 게이트다. X1 dry-run export 1차와 X2 비용 관측·예산 경고/evaluator 모델 override·semantic cache·hit-rate 관측·일별 비용 추이·모델별 비용 검증 리포트는 완료했고, 자동 차단·한도 enforcement는 정책 결정 전까지 보류한다. M1 provider_events 보존·taxonomy·인증 리뷰 파생 칩 4차, M2 case memory+pinned_fact 보수적 실적재·append-only history·명시적 상담약속 철회 contradiction·episodic embedding writer 6차, M3 claim 1차, L1 schema-backed engine gateway golden fixture·Node.js conformance runner·session read-model 분리는 완료. 후속 내부 후보는 persona DTO/mapper 분리와 H3 항목형 저작 UI다. 운영 SAML 서명검증은 기관 IdP 의존이다.</li></ul></div>
<divclass="detail-block"><h4>소유자 결정 / 외부(임상팀·기관) 의존</h4><ul><li>임상팀 산출물: C1 워크시트 항목·루브릭, C3 CBT 콘텐츠·체인, C2 위기 스크립트·서약 문안, H2 골든셋 코딩.</li><li>소유자 평가설계: H1 실험/통제군·3척도 문항·50분 흐름, κ/ICC·환각률 목표(doc4 미명시).</li><li>기관(한신 IT): M3 SSO IdP·test tenant·claim 스키마·인증서, 거버넌스 증거.</li><li>거버넌스: L1 Node 전환 방향은 결정됨. 남은 것은 FastAPI 유지 사유와 계약 우선 전환 계획의 저작권 등재 문서화·IP 협의.</li></ul></div>
<divclass="detail-block"><h4>소유자 결정 / 외부(임상팀·기관) 의존</h4><ul><li>임상팀 산출물: C1 워크시트 항목·루브릭, C3 CBT 콘텐츠·체인, C2 위기 스크립트·서약 문안, H2 골든셋 코딩.</li><li>소유자 평가설계: H1 실험/통제군·3척도 문항·50분 흐름, κ/ICC·환각률 목표(doc4 미명시).</li><li>기관(한신 IT): M3 SSO IdP·test tenant·claim 스키마·인증서, 거버넌스 증거.</li><li>거버넌스: L1 Node 전환 방향은 결정됨. 남은 것은 FastAPI 유지 사유와 계약 우선 전환 계획의 저작권 등재 문서화·IP 협의.</li></ul></div>
<divclass="detail-block"><h4>판독 한계 (정직성)</h4><ul><li>핵심 축어록 2단 표는 PDF 시각 판독이 1차 근거(txt 일부 누락), OCR 잔재 존재.</li><li>doc4 κ/ICC·환각률은 신청서 본문 미명시 — 계약 확정 지표로 단정 금지.</li><li>'현재상태'는 grep/코드 사실 기반. C1~C3·H2·H3은 직접 재확인(✓), 그 외는 착수 전 코드 1차 재확인 권고.</li></ul></div>
<divclass="detail-block"><h4>판독 한계 (정직성)</h4><ul><li>핵심 축어록 2단 표는 PDF 시각 판독이 1차 근거(txt 일부 누락), OCR 잔재 존재.</li><li>doc4 κ/ICC·환각률은 신청서 본문 미명시 — 계약 확정 지표로 단정 금지.</li><li>'현재상태'는 grep/코드 사실 기반에서 시작했으나 C1·C2·C3 2차 UI·H1~H4·M1 provider_events 보존/taxonomy/인증 리뷰 파생 노출·M2 case memory/pinned_fact/history/명시철회 contradiction/episodic embedding writer·M3·X1·X2·L1 Node conformance와 session read-model 분리는 구현 후 재검증 완료 기준이다. H3 원문 격리는 hash-only artifact와 RAG index 차단까지이며, 암호화 blob/vault 원문 실저장은 아직 후속이다. 실제 provider 기반 한숨·울음 감지, H4 legal/guardian evidence, M3 운영 IdP, L1 외부 거버넌스는 아직 게이트다.</li></ul></div>
</div>
</div>
</div>
</div>
@ -664,14 +696,14 @@
<divclass="scard-body"><divclass="kv"><b>현재</b><p>직접 WS 재현으로 근본원인 규명: 키 없으면 백엔드가 <code>degraded</code>+1011 즉시 close → 마이크 깜빡·녹음 미도달. 현재 로컬 <code>/voice/health</code>는 <code>available:true</code>이고, 실제 P2 회기에서 MP3 발화를 <code>/voice/ws</code>로 보내 <b>STT transcript 52자 → client reply 105자 → TTS 바이너리 66 chunks/266,496 bytes</b>를 수신했다.</p></div><divclass="kv"><b>부수 수정</b><p>① 미설정 시 마이크 버튼 사전 비활성+명확 안내 ② <b>TTS 지문 제거</b>: (고개 끄덕)·(한숨)·[침묵] 등 무대지시를 음성으로 안 읽음(<code>speakable_text</code>, 자막엔 유지) ③ <b>아바타 애니메이션</b>: 텍스트 턴 speaking 시 입 정지(0) 버그 수정 + thinking 표현 보강 ④ <b>브라우저 자동재생 차단 대응</b>: 마이크 클릭 시 <code>AudioContext.resume()</code>으로 unlock하고 TTS Blob을 Web Audio buffer source로 재생, 실패 시 기존 <code><audio></code> fallback. typecheck+voice테스트+voice E2E+live WS smoke PASS.</p></div></div>
<divclass="scard-body"><divclass="kv"><b>현재</b><p>직접 WS 재현으로 근본원인 규명: 키 없으면 백엔드가 <code>degraded</code>+1011 즉시 close → 마이크 깜빡·녹음 미도달. 현재 로컬 <code>/voice/health</code>는 <code>available:true</code>이고, 실제 P2 회기에서 MP3 발화를 <code>/voice/ws</code>로 보내 <b>STT transcript 52자 → client reply 105자 → TTS 바이너리 66 chunks/266,496 bytes</b>를 수신했다.</p></div><divclass="kv"><b>부수 수정</b><p>① 미설정 시 마이크 버튼 사전 비활성+명확 안내 ② <b>TTS 지문 제거</b>: (고개 끄덕)·(한숨)·[침묵] 등 무대지시를 음성으로 안 읽음(<code>speakable_text</code>, 자막엔 유지) ③ <b>아바타 애니메이션</b>: 텍스트 턴 speaking 시 입 정지(0) 버그 수정 + thinking 표현 보강 ④ <b>브라우저 자동재생 차단 대응</b>: 마이크 클릭 시 <code>AudioContext.resume()</code>으로 unlock하고 TTS Blob을 Web Audio buffer source로 재생, 실패 시 기존 <code><audio></code> fallback. typecheck+voice테스트+voice E2E+live WS smoke PASS.</p></div></div>
<buttonclass="scard-head"aria-expanded="false"><spanclass="chip c-done">REFACTOR 2 4차 DONE</span><spanclass="scard-mid"><spanclass="scard-title">백엔드 Pydantic ↔ 프론트 <code>lib/api.ts</code> 계약 SSOT화</span><spanclass="scard-sum">OpenAPI→<code>api.gen.ts</code> 생성 + CI stale 체크 게이트. 세션 계열 수기 DTO도 생성 타입 alias로 축소.</span></span><spanclass="caret"aria-hidden="true"></span></button>
<buttonclass="scard-head"aria-expanded="false"><spanclass="chip c-done">REFACTOR 2 4차 DONE</span><spanclass="scard-mid"><spanclass="scard-title">백엔드 Pydantic ↔ 프론트 <code>lib/api.ts</code> 계약 SSOT화</span><spanclass="scard-sum">OpenAPI→<code>api.gen.ts</code> 생성 + CI stale 체크 게이트. 세션 계열 수기 DTO도 생성 타입 alias로 축소.</span></span><spanclass="caret"aria-hidden="true"></span></button>
<divclass="scard-body"><divclass="kv"><b>현재</b><p><code>apps/api/scripts/export-openapi.py</code>와 <code>apps/web/scripts/generate-api-types.mjs</code>로 FastAPI OpenAPI를 <code>apps/web/src/lib/api.gen.ts</code>에 생성한다. <code>npm run check:api-types</code>가 재생성 누락을 잡는다. <code>MeResponse</code>, <code>AuthConfigResponse</code>, <code>ConsentResponse</code>, <code>PersonaSummary</code>, <code>PersonaReviewSummary</code>, admin/user/engine DTO, review leaf DTO, <code>CrisisResourceResponse</code>, teacher safety/growth leaf DTO, <code>LearnerSessionsResponse</code>, <code>SessionReviewResponse</code>, worksheet aggregate, <code>TeacherDashboardResponse</code>, <code>TeacherSessionSummary</code>, <code>TeacherLearnerGrowth</code>에 이어 <code>SessionStartResponse</code>, <code>SessionDetailResponse</code>, <code>SessionDetailTurn</code>도 <code>ApiSchema<...></code> alias로 전환했다. 세션 응답 stage는 backend <code>StageLabel</code> enum으로 고정해 OpenAPI가 <code>라포|탐색|개입|정리</code> union을 생성한다. generated optional/default 차이는 화면 form state, notification default helper, 배열 렌더링 fallback으로 흡수한다. <code>.github/workflows/api-contract.yml</code>은 PR/master push에서 같은 stale check를 실행해 OpenAPI type drift를 CI에서 막는다.</p></div><divclass="kv"><b>검증</b><p><code>npm run check:api-types</code>, <code>npm run typecheck</code>, <code>npm run build</code> passed. <code>python -m pytest app/test_session_turn_persistence.py -q</code> 16 passed. 영향 범위 E2E: <code>learner.spec.ts</code> + <code>session-review.spec.ts</code> + <code>teacher.spec.ts</code> + <code>layout-visual-gate.spec.ts</code> 32 passed.</p></div><divclass="kv k-warn"><b>잔여</b><p>평가 조회·리뷰 도식·교수 콘솔 등 아직 자유 문자열로 남긴 stage-like 필드는 각 도메인 의미를 확인한 뒤 별도 축소한다.</p></div></div>
<divclass="scard-body"><divclass="kv"><b>현재</b><p><code>apps/api/scripts/export-openapi.py</code>와 <code>apps/web/scripts/generate-api-types.mjs</code>로 FastAPI OpenAPI를 <code>apps/web/src/lib/api.gen.ts</code>에 생성한다. <code>npm run check:api-types</code>가 재생성 누락을 잡는다. <code>MeResponse</code>, <code>AuthConfigResponse</code>, <code>ConsentResponse</code>, <code>PersonaSummary</code>, <code>PersonaReviewSummary</code>, admin/user/engine DTO, review leaf DTO, <code>CrisisResourceResponse</code>, teacher safety/growth leaf DTO, <code>LearnerSessionsResponse</code>, <code>SessionReviewResponse</code>, worksheet aggregate, <code>TeacherDashboardResponse</code>, <code>TeacherSessionSummary</code>, <code>TeacherLearnerGrowth</code>에 이어 <code>SessionStartResponse</code>, <code>SessionDetailResponse</code>, <code>SessionDetailTurn</code>도 <code>ApiSchema<...></code> alias로 전환했다. 세션 응답 stage는 backend <code>StageLabel</code> enum으로 고정해 OpenAPI가 <code>라포|탐색|개입|정리</code> union을 생성한다. generated optional/default 차이는 화면 form state, notification default helper, 배열 렌더링 fallback으로 흡수한다. <code>.github/workflows/api-contract.yml</code>은 PR/master push에서 같은 stale check를 실행해 OpenAPI type drift를 CI에서 막는다.</p></div><divclass="kv"><b>검증</b><p><code>npm run check:api-types</code>, <code>npm run typecheck</code>, <code>npm run build</code> passed. <code>python -m pytest app/test_session_turn_persistence.py -q</code> 16 passed. 영향 범위 E2E: <code>learner.spec.ts</code> + <code>session-review.spec.ts</code> + <code>teacher.spec.ts</code> + <code>layout-visual-gate.spec.ts</code> 32 passed.</p></div><divclass="kv k-warn"><b>잔여</b><p>평가 조회·리뷰 도식·교수 콘솔 등 아직 자유 문자열로 남긴 stage-like 필드는 각 도메인 의미를 확인한 뒤 별도 축소한다.</p></div></div>
</article>
</article>
@ -680,12 +712,12 @@
<divclass="scard-body"><divclass="kv"><b>현재</b><p><code>apps/web/index.html</code>에 canonical, description, Open Graph/Twitter Card, JSON-LD(WebApplication)를 추가했고, <code>public/robots.txt</code>, <code>sitemap.xml</code>, <code>llms.txt</code>로 공개 사이트와 AI 검색용 설명을 정리했다. 인증 내부 경로는 robots에서 차단한다. 학습자 회기 리뷰는 종료 후 <code>POST /sessions/{id}/share</code>로 공개 토큰을 만들고, <code>GET /share/session/{token}</code>이 서버 렌더 HTML/OG/JSON-LD를 반환한다. <code>app.session_share_link</code>에는 토큰 해시와 sanitized preview payload만 저장하며 원문 축어록·학습자 식별자는 저장하지 않는다.</p></div><divclass="kv"><b>검증</b><p><code>pytest app/test_session_share.py app/test_session_turn_persistence.py -q</code> 21 passed, <code>python -m pytest app/ -q</code> 178 passed, <code>python -m pytest engine_gateway/ -q</code> 11 passed, <code>npm run check:api-types</code>, <code>npm run typecheck</code>, <code>npm run build</code> passed.</p></div><divclass="kv k-warn"><b>판정</b><p>공개 제품 페이지는 색인 가능, 회기 공유 페이지는 교수자 전달용 unfurl 카드라 <code>noindex</code>와 API robots <code>Disallow: /share/</code>를 유지한다. 실제 공개 도메인 unfurl 검증은 배포 후 1회 필요하다.</p></div></div>
<divclass="scard-body"><divclass="kv"><b>현재</b><p><code>apps/web/index.html</code>에 canonical, description, Open Graph/Twitter Card, JSON-LD(WebApplication)를 추가했고, <code>public/robots.txt</code>, <code>sitemap.xml</code>, <code>llms.txt</code>로 공개 사이트와 AI 검색용 설명을 정리했다. 인증 내부 경로는 robots에서 차단한다. 학습자 회기 리뷰는 종료 후 <code>POST /sessions/{id}/share</code>로 공개 토큰을 만들고, <code>GET /share/session/{token}</code>이 서버 렌더 HTML/OG/JSON-LD를 반환한다. <code>app.session_share_link</code>에는 토큰 해시와 sanitized preview payload만 저장하며 원문 축어록·학습자 식별자는 저장하지 않는다.</p></div><divclass="kv"><b>검증</b><p><code>pytest app/test_session_share.py app/test_session_turn_persistence.py -q</code> 21 passed, <code>python -m pytest app/ -q</code> 178 passed, <code>python -m pytest engine_gateway/ -q</code> 11 passed, <code>npm run check:api-types</code>, <code>npm run typecheck</code>, <code>npm run build</code> passed.</p></div><divclass="kv k-warn"><b>판정</b><p>공개 제품 페이지는 색인 가능, 회기 공유 페이지는 교수자 전달용 unfurl 카드라 <code>noindex</code>와 API robots <code>Disallow: /share/</code>를 유지한다. 실제 공개 도메인 unfurl 검증은 배포 후 1회 필요하다.</p></div></div>
<divclass="task-row"><div><spanclass="task-status s-done">DONE</span></div><div><b>compose OAuth 주입</b><p><code>ENVIRONMENT</code>, Google OAuth vars, <code>CORS_ORIGINS</code>, <code>OPENAI_BASE_URL</code>을 API 컨테이너에 명시했다.</p></div><div><b>산출물</b><p>compose 환경 주입 정리</p></div><div><b>검증</b><p><code>docker compose -f infra\docker-compose.yml --env-file infra\.env.example config --quiet</code> 더미 필수 env 세트로 통과</p></div></div>
<divclass="task-row"><div><spanclass="task-status s-done">DONE</span></div><div><b>compose OAuth 주입</b><p><code>ENVIRONMENT</code>, Google OAuth vars, <code>CORS_ORIGINS</code>, <code>OPENAI_BASE_URL</code>을 API 컨테이너에 명시했다.</p></div><div><b>산출물</b><p>compose 환경 주입 정리</p></div><div><b>검증</b><p><code>docker compose -f infra\docker-compose.yml --env-file infra\.env.example config --quiet</code> 더미 필수 env 세트로 통과</p></div></div>
<divclass="task-row"><div><spanclass="task-status s-done">DONE</span></div><div><b>admin 모바일 overflow 수정</b><p>긴 readiness detail을 안정화했고, 사용자 변경은 DB 저장소가 durable할 때만 허용한다.</p></div><div><b>산출물</b><p>admin responsive layout, durable store gate, AI health copy</p></div><div><b>검증</b><p>admin manage-users desktop/mobile 2 passed</p></div></div>
<divclass="task-row"><div><spanclass="task-status s-done">DONE</span></div><div><b>admin 모바일 overflow 수정</b><p>긴 readiness detail을 안정화했고, 사용자 변경은 DB 저장소가 durable할 때만 허용한다.</p></div><div><b>산출물</b><p>admin responsive layout, durable store gate, AI health copy</p></div><div><b>검증</b><p>admin manage-users desktop/mobile 2 passed</p></div></div>
<divclass="task-row"><div><spanclass="task-status s-done">DONE</span></div><div><b>global engine config 테스트 분리</b><p><code>db-persistence</code>는 전역 engine config를 바꾸므로 <code>@single-run</code>으로 분리했다.</p></div><div><b>산출물</b><p>Playwright single-run tagging</p></div><div><b>검증</b><p>전체 Playwright 72 passed</p></div></div>
<divclass="task-row"><div><spanclass="task-status s-done">DONE</span></div><div><b>global engine config 테스트 분리</b><p><code>db-persistence</code>는 전역 engine config를 바꾸므로 <code>@single-run</code>으로 분리했다.</p></div><div><b>산출물</b><p>Playwright single-run tagging</p></div><div><b>검증</b><p>전체 Playwright 113 passed</p></div></div>
<divclass="task-row"><div><spanclass="task-status s-done">DONE</span></div><div><b>handoff와 문서 갱신</b><p><code>docs/HANDOFF.md</code>, <code>docs/dev_dashboard.html</code>, <code>apps/web/e2e/README.md</code>를 최신 상태로 맞췄다.</p></div><div><b>산출물</b><p>handoff, dashboard, E2E README</p></div><div><b>검증</b><p>최신 커밋 기준 상태/검증/운영 포트 일치</p></div></div>
<divclass="task-row"><div><spanclass="task-status s-done">DONE</span></div><div><b>handoff와 문서 갱신</b><p><code>docs/HANDOFF.md</code>, <code>docs/dev_dashboard.html</code>, <code>apps/web/e2e/README.md</code>를 최신 상태로 맞췄다.</p></div><div><b>산출물</b><p>handoff, dashboard, E2E README</p></div><div><b>검증</b><p>최신 커밋 기준 상태/검증/운영 포트 일치</p></div></div>
<divclass="task-row"><div><spanclass="task-status s-done">DONE</span></div><div><b>공개 API prod 전환</b><p><code>cloudflared</code> ingress를 <code>127.0.0.1:8001</code> prod API로 돌렸고 public readiness gate가 통과했다.</p></div><div><b>산출물</b><p>prod API 8001, tunnel config</p></div><div><b>검증</b><p><code>https://api-vignette.chanpaca.net/health</code> prod/db/engine OK</p></div></div>
<divclass="task-row"><div><spanclass="task-status s-done">DONE</span></div><div><b>공개 API prod 전환</b><p><code>cloudflared</code> ingress를 <code>127.0.0.1:8001</code> prod API로 돌렸고 public readiness gate가 통과했다.</p></div><div><b>산출물</b><p>prod API 8001, tunnel config</p></div><div><b>검증</b><p><code>https://api-vignette.chanpaca.net/health</code> prod/db/engine OK</p></div></div>
<divclass="task-row"><div><spanclass="task-status s-done">DONE</span></div><div><b>공개 런타임 재기동 스크립트</b><p><code>scripts/start-public-runtime.ps1</code>와 <code>scripts/stop-public-runtime.ps1</code>로 API 8001과 cloudflared를 재현 가능하게 만들었다.</p></div><div><b>산출물</b><p>public runtime start/stop scripts</p></div><div><b>검증</b><p>engine 9099 + API 8001 + cloudflared only</p></div></div>
<divclass="task-row"><div><spanclass="task-status s-done">DONE</span></div><div><b>공개 런타임 재기동 스크립트</b><p><code>scripts/start-public-runtime.ps1</code>와 <code>scripts/stop-public-runtime.ps1</code>로 API 8001과 cloudflared를 재현 가능하게 만들었다.</p></div><div><b>산출물</b><p>public runtime start/stop scripts</p></div><div><b>검증</b><p>engine 9099 + API 8001 + cloudflared only</p></div></div>
<divclass="detail-head"><div><h3>Phase 1 MVP 코어 루프 상세</h3><p>목표는 "P1 내담자와 실제 상담 1회 완주 후 피드백을 받는 공개 시연 가능 상태"다.</p></div><divclass="mini-metrics"><span>우선순위: P0</span><span>범위: 텍스트+음성 1턴</span><span>완료보고: E2E 증거 후</span></div></div>
<divclass="detail-head"><div><h3>Phase 1 MVP 코어 루프 상세</h3><p>목표는 "P1 내담자와 실제 상담 1회 완주 후 피드백을 받는 공개 시연 가능 상태"다.</p></div><divclass="mini-metrics"><span>우선순위: P0</span><span>범위: 텍스트+음성 1턴</span><span>완료보고: E2E 증거 후</span></div></div>
<divclass="task-list">
<divclass="task-list">
<divclass="task-row"><div><spanclass="task-status s-doing">CHECK</span></div><div><b>LLMEngine 정규화 계약 고정</b><p>gateway SSE <code>token/done/error</code>와 generate cost 계약은 회귀화했다. 이번 패스에서 <code>app/contracts/engine_gateway.py</code>를 추가해 FastAPI <code>engine_client</code>와 현재 Python gateway가 같은 <code>GenerateRequest</code>/<code>GenerateResponse</code>/<code>Stream*Event</code> 계약을 import한다. Node.js gateway로 갈아치울 때 보존해야 할 첫 wire contract다. claude_cli와 Messages API 폴백 동일성 검증은 라이브 키/환경 의존이라 남아 있다.</p></div><div><b>산출물</b><p><code>app/contracts/engine_gateway.py</code>, <code>engine_gateway/test_gateway_model.py</code> shared contract + stream frame tests</p></div><div><b>검증</b><p><code>python -B -m py_compile app/contracts/engine_gateway.py app/engine_client.py engine_gateway/gateway.py</code>, <code>python -B -m pytest -p no:cacheprovider engine_gateway/test_gateway_model.py -q</code> 11 passed.</p></div></div>
<divclass="task-row"><div><spanclass="task-status s-done">DONE</span></div><div><b>LLMEngine 정규화 계약 고정</b><p>gateway SSE <code>token/done/error</code>와 generate cost 계약은 회귀화했다. 이번 패스에서 <code>app/contracts/engine_gateway.py</code>를 추가해 FastAPI <code>engine_client</code>와 현재 Python gateway가 같은 <code>GenerateRequest</code>/<code>GenerateResponse</code>/<code>Stream*Event</code> 계약을 import한다. 이어서 <code>EngineGatewaySseLineDecoder</code>로 token/done/error decode와 validation도 같은 계약 모듈에 모았다. 최신 패스에서는 raw SSE line 해석을 <code>EngineClient.stream_packets()</code> 경계로 올리고, Python gateway <code>/v1/generate</code>가 <code>GenerateResponse.model_dump()</code>로 응답하게 해 hand-mirrored dict drift를 줄였다. Node.js gateway로 갈아치울 때 보존해야 할 첫 wire contract다. claude_cli와 Messages API live parity는 <code>ANTHROPIC_API_KEY</code> 의존 B2 항목으로 분리한다.</p></div><div><b>산출물</b><p><code>app/contracts/engine_gateway.py</code>, <code>app/engine_client.py</code>, <code>app/services/orchestrator.py</code>, <code>engine_gateway/gateway.py</code>, <code>engine_gateway/test_gateway_model.py</code>, <code>engine_gateway/golden/*</code>, <code>scripts/check-engine-gateway-contract.mjs</code> shared contract + stream packet tests</p></div><div><b>검증</b><p><code>node scripts/check-engine-gateway-contract.mjs --json</code>, <code>python -B -m pytest -p no:cacheprovider engine_gateway/test_gateway_model.py -q</code> 19 passed, <code>python -B -m pytest -p no:cacheprovider engine_gateway/test_gateway_model.py app/test_orchestrator_masking.py app/test_session_turn_persistence.py -q</code> 50 passed.</p></div></div>
<divclass="task-row"><div><spanclass="task-status s-done">DONE</span></div><div><b>상주 <code>claude -p</code> 엔진풀 실증</b><p><code>session_id</code>가 살아 있는 gateway session을 재사용하고 missing session은 ephemeral로 닫히는 구조를 회귀화했고, live 게이트웨이(9099)로 실측까지 마쳤다. <code>probe-engine-gateway.py</code>가 단일 session_id로 2회 reused stream을 돌려 TTFT 1667–4199ms, cost 누적 0.068→0.123(turns 1→2), engine=claude_p, model=opus-4-8을 측정했다.</p></div><div><b>산출물</b><p><code>engine_gateway/test_gateway_model.py</code>, <code>scripts/probe-engine-gateway.py</code>, live probe JSON</p></div><div><b>검증</b><p>session reuse 7 tests OK; live probe 2 reused streams, TTFT/cost 실측, session_id 재사용 확인</p></div></div>
<divclass="task-row"><div><spanclass="task-status s-done">DONE</span></div><div><b>상주 <code>claude -p</code> 엔진풀 실증</b><p><code>session_id</code>가 살아 있는 gateway session을 재사용하고 missing session은 ephemeral로 닫히는 구조를 회귀화했고, live 게이트웨이(9099)로 실측까지 마쳤다. <code>probe-engine-gateway.py</code>가 단일 session_id로 2회 reused stream을 돌려 TTFT 1667–4199ms, cost 누적 0.068→0.123(turns 1→2), engine=claude_p, model=opus-4-8을 측정했다.</p></div><div><b>산출물</b><p><code>engine_gateway/test_gateway_model.py</code>, <code>scripts/probe-engine-gateway.py</code>, live probe JSON</p></div><div><b>검증</b><p>session reuse 7 tests OK; live probe 2 reused streams, TTFT/cost 실측, session_id 재사용 확인</p></div></div>
<divclass="task-row"><div><spanclass="task-status s-done">DONE</span></div><div><b>마스킹 게이트 + LLM call audit</b><p>current turn, recall, pinned fact, recent turns가 Presidio/가명처리 후에만 engine request로 들어가게 막았다. 외부 LLM 호출은 <code>audit.llm_call_log</code>에 provider/model/token/cost/inference_geo/latency metadata만 남기고 prompt/completion 본문은 저장하지 않는다.</p></div><div><b>산출물</b><p><code>app/test_orchestrator_masking.py</code>, <code>app/test_evaluation_persistence.py</code>, <code>session_persistence.record_llm_call_audit</code></p></div><div><b>검증</b><p>phone/email/RRN raw 값이 generate/stream payload에 없음, 감사 payload 본문 미포함, 관련 회귀 27 passed, 전체 API 최신 130 passed, 로컬 live <code>/turn</code> smoke 후 <code>audit.llm_call_log</code> 8→11</p></div></div>
<divclass="task-row"><div><spanclass="task-status s-done">DONE</span></div><div><b>마스킹 게이트 + LLM call audit</b><p>current turn, recall, pinned fact, recent turns가 Presidio/가명처리 후에만 engine request로 들어가게 막았다. 한국어 이름/기관 로컬 휴리스틱 1차와 합성 fixture 평가 harness를 추가했고, fast evaluator prompt와 client turn <code>text_masked</code>도 마스킹본을 쓰게 보강했다. 외부 LLM 호출은 <code>audit.llm_call_log</code>에 provider/model/token/cost/inference_geo/latency metadata만 남기고 prompt/completion 본문은 저장하지 않는다.</p></div><div><b>산출물</b><p><code>app/services/pii_masking_eval.py</code>, <code>data/privacy/pii-masking-ko-fixtures.json</code>, <code>scripts/evaluate-pii-masking.py</code>, <code>app/test_pii_masking_eval.py</code>, <code>app/test_orchestrator_masking.py</code>, <code>app/test_evaluation_persistence.py</code>,<code>app/test_session_turn_persistence.py</code>,<code>session_persistence.record_llm_call_audit</code></p></div><div><b>검증</b><p>phone/email/RRN 및 한국어 NAME/ORG raw 값이 generate/stream/evaluator payload에 없음, 감사 payload 본문 미포함, fixture 5/5 pass, focused 회귀 41 passed, 현재 전체 API 178 기준 유지, 로컬 live <code>/turn</code> smoke 후 <code>audit.llm_call_log</code> 8→11</p></div></div>
<divclass="task-row"><div><spanclass="task-status s-doing">CHECK</span></div><div><b>P1 상담 완주 플로우</b><p>통제된 UI E2E로 P1 선택, SSE 상담, 종료, 회기말 리뷰 피드백을 한 번에 통과시켰다. 실제 운영 로그인 proof는 P0 blocker와 함께 남아 있다.</p></div><div><b>산출물</b><p><code>e2e/session-mvp.spec.ts</code>, turn telemetry 저장 경로</p></div><div><b>검증</b><p><code>chromium-single-run</code> P1 MVP UI E2E 1 passed</p></div></div>
<divclass="task-row"><div><spanclass="task-status s-done">DONE</span></div><div><b>P1 상담 완주 플로우</b><p>로컬 DB-backed 런타임에서 P1 선택, SSE 상담, 종료, 회기말 리뷰 피드백을 한 번에 통과시켰다. 실제 운영 로그인 proof는 P0 public OAuth blocker와 분리해 남긴다.</p></div><div><b>산출물</b><p><code>e2e/session-mvp.spec.ts</code>, turn telemetry 저장 경로</p></div><div><b>검증</b><p><code>npx playwright test e2e/session-mvp.spec.ts --project=chromium-single-run --workers=1</code> 1 passed.</p></div></div>
<divclass="task-row"><div><spanclass="task-status s-done">DONE</span></div><div><b>저항 엔진 정량 검증</b><p>공감 발화와 조언점프 발화 5턴 세트를 상태머신 회귀 테스트와 실제 API+DB smoke로 비교했다. live smoke는 dev-login→onboarding→P1 세션 2개→각 5턴을 수행하고 <code>app.session_state</code>, <code>app.turns</code>, <code>app.turn_client_state</code>를 직접 조회했다. 공감 세션은 3턴부터 <code>탐색</code>으로 열리고 openness가 <code>0.171</code>까지 상승했다. 조언점프 세션은 <code>라포</code>와 openness <code>0.0</code>을 유지했다.</p></div><div><b>산출물</b><p><code>app/test_state_machine_resistance.py</code>, <code>scripts/smoke-resistance-openness-db.py</code>, <code>docs/ops/resistance-openness-db-smoke-2026-06-28.json</code></p></div><div><b>검증</b><p><code>pytest app/test_state_machine_resistance.py app/test_evaluation_persistence.py -q</code> 9 passed; live DB smoke PASS.</p></div></div>
<divclass="task-row"><div><spanclass="task-status s-done">DONE</span></div><div><b>저항 엔진 정량 검증</b><p>공감 발화와 조언점프 발화 5턴 세트를 상태머신 회귀 테스트와 실제 API+DB smoke로 비교했다. live smoke는 dev-login→onboarding→P1 세션 2개→각 5턴을 수행하고 <code>app.session_state</code>, <code>app.turns</code>, <code>app.turn_client_state</code>를 직접 조회했다. 공감 세션은 3턴부터 <code>탐색</code>으로 열리고 openness가 <code>0.171</code>까지 상승했다. 조언점프 세션은 <code>라포</code>와 openness <code>0.0</code>을 유지했다.</p></div><div><b>산출물</b><p><code>app/test_state_machine_resistance.py</code>, <code>scripts/smoke-resistance-openness-db.py</code>, <code>docs/ops/resistance-openness-db-smoke-2026-06-28.json</code></p></div><div><b>검증</b><p><code>pytest app/test_state_machine_resistance.py app/test_evaluation_persistence.py -q</code> 9 passed; live DB smoke PASS.</p></div></div>
<divclass="task-row"><div><spanclass="task-status s-done">DONE</span></div><div><b>음성 왕복 1턴 PoC</b><p>Session UI 마이크 버튼 경로에서 synthetic <code>getUserMedia</code>/<code>MediaRecorder</code>로 실제 <code>/voice/ws</code>를 열고 audio_start, binary chunk, audio_end, transcript, reply, TTS 재생 상태까지 검증했다. 추가로 로컬 live API에서 실제 STT/TTS provider를 통과해 TTS 바이너리를 수신했다. 물리 마이크, 공개 WSS, 50분 양방향 실측은 별도 운영 smoke로 남아 있다.</p></div><div><b>산출물</b><p><code>apps/web/e2e/voice-success.spec.ts</code>, controlled OpenAI/engine providers, Web Audio browser voice UI probe, live <code>/voice/ws</code> smoke</p></div><div><b>검증</b><p><code>npx playwright test e2e/voice-success.spec.ts --project=chromium-single-run</code> 2 passed; live P2 WS smoke binary 66 chunks/266,496 bytes</p></div></div>
<divclass="task-row"><div><spanclass="task-status s-done">DONE</span></div><div><b>음성 왕복 1턴 PoC</b><p>Session UI 마이크 버튼 경로에서 synthetic <code>getUserMedia</code>/<code>MediaRecorder</code>로 실제 <code>/voice/ws</code>를 열고 audio_start, binary chunk, audio_end, transcript, reply, TTS 재생 상태까지 검증했다. 추가로 로컬 live API에서 실제 STT/TTS provider를 통과해 TTS 바이너리를 수신했다. 물리 마이크, 공개 WSS, 50분 양방향 실측은 별도 운영 smoke로 남아 있다.</p></div><div><b>산출물</b><p><code>apps/web/e2e/voice-success.spec.ts</code>, controlled OpenAI/engine providers, Web Audio browser voice UI probe, live <code>/voice/ws</code> smoke</p></div><div><b>검증</b><p><code>npx playwright test e2e/voice-success.spec.ts --project=chromium-single-run</code> 2 passed; live P2 WS smoke binary 66 chunks/266,496 bytes</p></div></div>
<divclass="detail-head"><div><h3>Phase 2a 권한, OAuth, 페르소나 상세</h3><p>음성과 섞지 않고 3역할, 권한격리, 페르소나 확장을 독립 트랙으로 닫는다.</p></div><divclass="mini-metrics"><span>주차: 9-13</span><span>우선순위: P1</span><span>IDOR 검증 필수</span></div></div>
<divclass="detail-head"><div><h3>Phase 2a 권한, OAuth, 페르소나 상세</h3><p>음성과 섞지 않고 3역할, 권한격리, 페르소나 확장을 독립 트랙으로 닫는다.</p></div><divclass="mini-metrics"><span>주차: 9-13</span><span>우선순위: P1</span><span>IDOR 검증 필수</span></div></div>
<divclass="task-list">
<divclass="task-list">
<divclass="task-row"><div><spanclass="task-status s-doing">CHECK</span></div><div><b>OAuth BFF / HttpOnly 세션 완성</b><p>Auth Code + PKCE/state와 opaque HttpOnly/Secure cookie 계약을 회귀화했다. OAuth state는 HMAC 서명 토큰과 HttpOnly state cookie를 함께 검증해 API 재시작 뒤 콜백이 <code>invalid_state</code>로 떨어지지 않으면서도 브라우저 시작 흐름에 묶인다. Google provider error는 <code>access_denied</code>/<code>provider_error</code>로 분리하고 로그인 화면에 reason code를 표시한다. 공개 Google OAuth 실제 <code>/turn</code> proof는 P0 blocker로 남아 있다.</p></div><div><b>산출물</b><p><code>app/test_auth_providers.py</code>, cookie-bound signed PKCE state, OAuth callback cookie path, login reason UI</p></div><div><b>검증</b><p>auth URL secret 미노출, browser token 미저장, dev/prod cookie 속성 분리, state cookie 불일치 차단, 재시작 후 signed state 콜백 복구, provider error reason 분리</p></div></div>
<divclass="task-row"><div><spanclass="task-status s-done">DONE</span></div><div><b>OAuth BFF / HttpOnly 세션 완성</b><p>Auth Code + PKCE/state와 opaque HttpOnly/Secure cookie 계약을 회귀화했다. OAuth state는 HMAC 서명 토큰과 HttpOnly state cookie를 함께 검증해 API 재시작 뒤 콜백이 <code>invalid_state</code>로 떨어지지 않으면서도 브라우저 시작 흐름에 묶인다. Google provider error는 <code>access_denied</code>/<code>provider_error</code>로 분리하고 로그인 화면에 reason code를 표시한다. 공개 Google OAuth 실제 <code>/turn</code> proof는 P0 blocker로 남아 있다.</p></div><div><b>산출물</b><p><code>app/test_auth_providers.py</code>, cookie-bound signed PKCE state, OAuth callback cookie path, login reason UI</p></div><div><b>검증</b><p><code>python -B -m pytest -p no:cacheprovider app/test_auth_providers.py -q</code> 30 passed; <code>npm run typecheck</code> passed.</p></div></div>
<divclass="task-row"><div><spanclass="task-status s-done">DONE</span></div><div><b>학습자/교수자/관리자 권한 격리</b><p>runtime fallback에서 타 learner session 직접 접근 403과 learner list 필터를 회귀화했고, teacher/admin durable read audit insert 경로를 추가했다. live Postgres(NOBYPASSRLS <code>vignette_app</code> 역할)로 RLS/audit smoke를 실행해 운영 증거를 확보했다: learner 세션/턴 격리, teacher cohort 스코프, teacher/admin read_session audit insert가 실제 정책 하에서 PASS. 이번 패스에서 역할(role)과 관리자 페이지 진입권(<code>admin_access</code>)을 분리했고, <code>AUTH_SUPER_ADMIN_EMAILS</code> 슈퍼 관리자만 관리자 권한을 부여·회수할 수 있게 했다. 슈퍼 관리자는 학습자/교수자 라우트도 통과할 수 있지만, 일반 <code>admin_access</code>는 관리자 콘솔 진입권으로만 쓰인다.</p></div><div><b>산출물</b><p><code>app_user.admin_access</code>, <code>require_admin_access()</code>, <code>/admin/users</code> 권한 위임 UI, <code>app/test_rbac_idor.py</code>, <code>audit.audit_log</code> read_session insert, <code>scripts/check-postgres-rls-audit.py</code>(live PASS)</p></div><div><b>검증</b><p>타인 <code>session_id</code> 직접 입력 403(unit), live RLS smoke 5 checks PASS, 관리자 권한 위임 회귀 <code>app/test_auth_providers.py app/test_admin_ops.py</code> 32 passed</p></div></div>
<divclass="task-row"><div><spanclass="task-status s-done">DONE</span></div><div><b>학습자/교수자/관리자 권한 격리</b><p>runtime fallback에서 타 learner session 직접 접근 403과 learner list 필터를 회귀화했고, teacher/admin durable read audit insert 경로를 추가했다. live Postgres(NOBYPASSRLS <code>vignette_app</code> 역할)로 RLS/audit smoke를 실행해 운영 증거를 확보했다: learner 세션/턴 격리, teacher cohort 스코프, teacher/admin read_session audit insert가 실제 정책 하에서 PASS. 이번 패스에서 역할(role)과 관리자 페이지 진입권(<code>admin_access</code>)을 분리했고, <code>AUTH_SUPER_ADMIN_EMAILS</code> 슈퍼 관리자만 관리자 권한을 부여·회수할 수 있게 했다. 슈퍼 관리자는 학습자/교수자 라우트도 통과할 수 있지만, 일반 <code>admin_access</code>는 관리자 콘솔 진입권으로만 쓰인다.</p></div><div><b>산출물</b><p><code>app_user.admin_access</code>, <code>require_admin_access()</code>, <code>/admin/users</code> 권한 위임 UI, <code>app/test_rbac_idor.py</code>, <code>audit.audit_log</code> read_session insert, <code>scripts/check-postgres-rls-audit.py</code>(live PASS)</p></div><div><b>검증</b><p>타인 <code>session_id</code> 직접 입력 403(unit), live RLS smoke 5 checks PASS, 관리자 권한 위임 회귀 <code>app/test_auth_providers.py app/test_admin_ops.py</code> 32 passed</p></div></div>
<divclass="task-row"><div><spanclass="task-status s-done">DONE</span></div><div><b><code>visible_to</code>와 인간 권한 레이어 분리</b><p>learner detail/review와 client engine history에서 evaluator-only turn을 제외한다. live Postgres smoke가 evaluator-only DB 정책을 실제로 검사해 PASS: evaluator-only turn이 learner/client/counselor AI에는 숨겨지고 evaluator AI에만 보인다.</p></div><div><b>산출물</b><p><code>TurnRecord.visible_to</code>, learner/client/evaluator filters, <code>docs/ops/postgres-rls-audit-smoke.md</code></p></div><div><b>검증</b><p>evaluator-only turn 숨김 unit proof + live RLS smoke <code>visible_to.evaluator_only_hidden</code> PASS</p></div></div>
<divclass="task-row"><div><spanclass="task-status s-done">DONE</span></div><div><b><code>visible_to</code>와 인간 권한 레이어 분리</b><p>learner detail/review와 client engine history에서 evaluator-only turn을 제외한다. live Postgres smoke가 evaluator-only DB 정책을 실제로 검사해 PASS: evaluator-only turn이 learner/client/counselor AI에는 숨겨지고 evaluator AI에만 보인다.</p></div><div><b>산출물</b><p><code>TurnRecord.visible_to</code>, learner/client/evaluator filters, <code>docs/ops/postgres-rls-audit-smoke.md</code></p></div><div><b>검증</b><p>evaluator-only turn 숨김 unit proof + live RLS smoke <code>visible_to.evaluator_only_hidden</code> PASS</p></div></div>
<divclass="task-row"><div><spanclass="task-status s-done">DONE</span></div><div><b>페르소나 P2/P3 교수 검수 루프</b><p>approved catalog와 draft/review 교수 검수 큐를 분리하고, teacher/admin 승인·반려 mutation과 교수 콘솔 UI proof를 추가했다. 교수 콘솔의 404 raw 노출도 제거(404→정상 빈상태)했다.</p></div><div><b>산출물</b><p><code>/personas/review</code>, <code>POST /personas/review/{persona_id}</code>, professor review panel</p></div><div><b>검증</b><p><code>app.test_persona_review</code> 14 tests; teacher UI single-run E2E 1 passed; live RLS smoke PASS</p></div></div>
<divclass="task-row"><div><spanclass="task-status s-done">DONE</span></div><div><b>페르소나 P2/P3 교수 검수 루프</b><p>approved catalog와 draft/review 교수 검수 큐를 분리하고, teacher/admin 승인·반려 mutation과 교수 콘솔 UI proof를 추가했다. 교수 콘솔의 404 raw 노출도 제거(404→정상 빈상태)했다.</p></div><div><b>산출물</b><p><code>/personas/review</code>, <code>POST /personas/review/{persona_id}</code>, professor review panel</p></div><div><b>검증</b><p><code>app.test_persona_review</code> 14 tests; teacher UI single-run E2E 1 passed; live RLS smoke PASS</p></div></div>
<divclass="task-row"><div><spanclass="task-status s-doing">CHECK</span></div><div><b>SAML SP 스캐폴드 선반영</b><p>Google OIDC와 별도 provider 상태를 <code>/auth/config</code>에 노출하고, Redirect-binding AuthnRequest와 dev-only unsigned fixture ACS를 backend proof 수준으로 구현했다. 서명 검증이 필요한 운영 SAML ACS는 아직 미구현이며 fingerprint 설정 시 거부한다.</p></div><div><b>산출물</b><p><code>AUTH_SAML_ENABLED</code>, <code>SAML_SP_ENTITY_ID</code>, <code>SAML_SSO_URL</code>, RelayState store, <code>/auth/saml/acs</code> fixture endpoint</p></div><div><b>검증</b><p>AuthnRequest issuer/ACS/destination, RelayState 검증, opaque HttpOnly cookie, bad RelayState/signature-required reject unit test</p></div></div>
<divclass="task-row"><div><spanclass="task-status s-done">DONE</span></div><div><b>SAML SP 스캐폴드 선반영</b><p>Google OIDC와 별도 provider 상태를 <code>/auth/config</code>에 노출하고, Redirect-binding AuthnRequest와 dev-only unsigned fixture ACS를 backend proof 수준으로 구현했다. 운영 SAML 서명검증과 기관 test tenant는 외부 IdP 증거가 필요하며, fingerprint 설정 시 unsigned fixture ACS는 거부한다.</p></div><div><b>산출물</b><p><code>AUTH_SAML_ENABLED</code>, <code>SAML_SP_ENTITY_ID</code>, <code>SAML_SSO_URL</code>, RelayState store, <code>/auth/saml/acs</code> fixture endpoint</p></div><div><b>검증</b><p><code>app/test_auth_providers.py</code> 30 passed: AuthnRequest issuer/ACS/destination, RelayState 검증, opaque HttpOnly cookie, bad RelayState/signature-required reject.</p></div></div>
<divclass="detail-head"><div><h3>Phase 2b 음성 캐스케이드 상세</h3><p>음성은 "되게 보이는 UI"가 아니라, 전송, STT, EOT, 엔진, TTS, 저장까지 끊기지 않아야 한다.</p></div><divclass="mini-metrics"><span>주차: 13-17</span><span>WSS 50분</span><span>실측 전 완료 금지</span></div></div>
<divclass="detail-head"><div><h3>Phase 2b 음성 캐스케이드 상세</h3><p>음성은 "되게 보이는 UI"가 아니라, 전송, STT, EOT, 엔진, TTS, 저장까지 끊기지 않아야 한다.</p></div><divclass="mini-metrics"><span>주차: 13-17</span><span>WSS 50분</span><span>실측 전 완료 금지</span></div></div>
<divclass="task-list">
<divclass="task-list">
<divclass="task-row"><div><spanclass="task-status s-doing">CHECK</span></div><div><b>voice gateway WebSocket</b><p><code>/voice/ws</code> backend contract를 fake WebSocket으로 회귀화했고, Session UI의 MediaRecorder 기반 브라우저 경로도 synthetic audio로 한 턴 통과시켰다. 실제 AudioWorklet 전환, public WSS, 50분 양방향 실측은 남아 있다.</p></div><div><b>산출물</b><p><code>app/test_voice_ws.py</code>, <code>apps/web/e2e/voice-success.spec.ts</code>, audio_start/binary/audio_end/text_turn/ping contract</p></div><div><b>검증</b><p>5 backend tests OK; voice-success chromium-single-run 2 passed; auth guard, chunk concat, max audio cap, TTS chunk envelope</p></div></div>
<divclass="task-row"><div><spanclass="task-status s-doing">CHECK</span></div><div><b>voice gateway WebSocket</b><p><code>/voice/ws</code> backend contract를 fake WebSocket으로 회귀화했고, Session UI의 MediaRecorder 기반 브라우저 경로도 synthetic audio로 한 턴 통과시켰다. 실제 AudioWorklet 전환, public WSS, 50분 양방향 실측은 남아 있다.</p></div><div><b>산출물</b><p><code>app/test_voice_ws.py</code>, <code>apps/web/e2e/voice-success.spec.ts</code>, audio_start/binary/audio_end/text_turn/ping contract</p></div><div><b>검증</b><p>8 backend tests OK; voice-success chromium-single-run 2 passed; auth guard, chunk concat, max audio cap, TTS chunk envelope</p></div></div>
<divclass="task-row"><div><spanclass="task-status s-doing">CHECK</span></div><div><b>Deepgram STT + EOT</b><p>provider-neutral EOT helper로 final transcript와 silence threshold readiness를 회귀화했다. 실제 Deepgram interim/final 스트림 연결은 남아 있다.</p></div><div><b>산출물</b><p><code>assess_end_of_turn</code>, <code>EndOfTurnDecision</code>, EOT silence threshold</p></div><div><b>검증</b><p>final pending, empty transcript, silence threshold 케이스 unit test</p></div></div>
<divclass="task-row"><div><spanclass="task-status s-doing">CHECK</span></div><div><b>Deepgram STT + EOT</b><p>provider-neutral EOT helper로 final transcript와 silence threshold readiness를 회귀화했다. 실제 Deepgram interim/final 스트림 연결은 남아 있다.</p></div><div><b>산출물</b><p><code>assess_end_of_turn</code>, <code>EndOfTurnDecision</code>, EOT silence threshold</p></div><div><b>검증</b><p>final pending, empty transcript, silence threshold 케이스 unit test</p></div></div>
<divclass="task-row"><div><spanclass="task-status s-done">DONE</span></div><div><b>OpenAI TTS voice preset 추상화</b><p>P1/P2/P3 persona voice preset, invalid preset fallback, speed clamp, instructions payload 계약을 회귀화했고, <code>app.persona_voice_map</code>을 live <code>/voice/ws</code> TTS 선택에 연결했다. 명시 preset은 DB map보다 우선하고, OpenAI가 아닌 provider row는 기존 fallback으로 안전하게 흡수한다.</p></div><div><b>산출물</b><p><code>resolve_voice</code>, <code>resolve_voice_from_map</code>, <code>build_tts_payload</code>, <code>get_session_voice_map</code>, seed voice map row, dev runtime schema guard</p></div><div><b>검증</b><p><code>pytest app/test_runtime_policy.py app/test_persona_review.py app/test_voice_service.py app/test_voice_ws.py -q</code> 75 passed; full API 175 passed + gateway 9 passed.</p></div></div>
<divclass="task-row"><div><spanclass="task-status s-done">DONE</span></div><div><b>OpenAI TTS voice preset 추상화</b><p>P1/P2/P3 persona voice preset, invalid preset fallback, speed clamp, instructions payload 계약을 회귀화했고, <code>app.persona_voice_map</code>을 live <code>/voice/ws</code> TTS 선택에 연결했다. 명시 preset은 DB map보다 우선하고, OpenAI가 아닌 provider row는 기존 fallback으로 안전하게 흡수한다.</p></div><div><b>산출물</b><p><code>resolve_voice</code>, <code>resolve_voice_from_map</code>, <code>build_tts_payload</code>, <code>get_session_voice_map</code>, seed voice map row, dev runtime schema guard</p></div><div><b>검증</b><p><code>pytest app/test_runtime_policy.py app/test_persona_review.py app/test_voice_service.py app/test_voice_ws.py -q</code> 75 passed; full API 178 passed + gateway 11 passed.</p></div></div>
<divclass="task-row"><div><spanclass="task-status s-plan">POC</span></div><div><b>P1 서연 무참조 synthetic voice 샘플</b><p>Higgs v3 로컬 런타임에서 실존 reference voice 없이 synthetic seed를 먼저 만들고, 그 seed만 재참조해 우울·피로·불안·라포·회복감 5개 변주를 생성했다. 개발용 <code>VIGNETTE_VOICE_POC_SAMPLE_TTS</code> 플래그로 <code>/voice/ws</code> P1 sample TTS provider에도 연결했다.</p></div><div><b>산출물</b><p><code>docs/voice-art/bimec-persona-voice-poc-2026-06-27.md</code>, <code>docs/voice-art/p1-seoyeon-higgs-v3-20260627/manifest.json</code>, <code>apps/api/app/services/voice.py</code></p></div><div><b>검증</b><p>mp3/wav 6개(seed+5) 생성, 24kHz, 7.72~11.68초. live <code>/voice/ws</code> text_turn smoke: provider <code>p1-sample-poc</code>, binary 20 chunks/78,573 bytes. 샘플 provider는 TTS override만 담당하고 마이크/STT는 OpenAI 키가 필요하다. 프로덕션 탑재는 모델 라이선스·권리정책 결정 전 금지.</p></div></div>
<divclass="task-row"><div><spanclass="task-status s-done">POC DONE</span></div><div><b>P1 서연 무참조 synthetic voice 샘플</b><p>Higgs v3 로컬 런타임에서 실존 reference voice 없이 synthetic seed를 먼저 만들고, 그 seed만 재참조해 우울·피로·불안·라포·회복감 5개 변주를 생성했다. 개발용 <code>VIGNETTE_VOICE_POC_SAMPLE_TTS</code> 플래그로 <code>/voice/ws</code> P1 sample TTS provider에도 연결했다.</p></div><div><b>산출물</b><p><code>docs/voice-art/bimec-persona-voice-poc-2026-06-27.md</code>, <code>docs/voice-art/p1-seoyeon-higgs-v3-20260627/manifest.json</code>, <code>apps/api/app/services/voice.py</code></p></div><div><b>검증</b><p>mp3/wav 6개(seed+5) 생성, 24kHz, 7.72~11.68초. live <code>/voice/ws</code> text_turn smoke: provider <code>p1-sample-poc</code>, binary 20 chunks/78,573 bytes. 샘플 provider는 TTS override만 담당하고 마이크/STT는 OpenAI 키가 필요하다. 프로덕션 탑재는 모델 라이선스·권리정책 결정 전 금지.</p></div></div>
<divclass="task-row"><div><spanclass="task-status s-doing">CHECK</span></div><div><b>barge-in / paralinguistic logging</b><p>음성 learner turn에 audio_ref, silence_ms, speech_rate, barge_in을 nullable 메타로 저장하고, 회기 리뷰에서 침묵·발화 속도·끼어듦·음성 입력 비언어 칩으로 표시한다. 실제 장시간 WSS/마이크와 한숨·울음 감지 증거는 남아 있다.</p></div><div><b>산출물</b><p><code>turns</code> nullable 확장, voice route 메타 산출, <code>ReviewNonverbalEvent</code>, <code>SessionReview</code> 칩 UI</p></div><div><b>검증</b><p>오디오 STT→엔진→TTS 더블 테스트에서 voice turn log에 메타 저장; session review E2E 1 passed; backend voice/session 17 passed</p></div></div>
<divclass="task-row"><div><spanclass="task-status s-doing">CHECK</span></div><div><b>barge-in / paralinguistic logging</b><p>음성 learner turn에 audio_ref, silence_ms, speech_rate, barge_in을 nullable 메타로 저장하고, 회기 리뷰에서 침묵·발화 속도·끼어듦·음성 입력 비언어 칩으로 표시한다. 2차로 <code>provider_events</code> JSONB 보존 슬롯을 추가해 control/STT provider 이벤트를 allowlist·size limit 후 저장하고, 3차로 내부 taxonomy <code>event_type</code>/<code>category</code>를 붙인다. 4차로 인증된 회기 리뷰에는 한숨·울음·웃음·호흡·운율·배경소음 같은 정규화 이벤트만 label/detail 칩으로 제한 노출한다. raw transcript/text/provider/source/raw type은 응답에서 제외하고, 공개 공유 카드는 축어록과 provider raw를 계속 싣지 않는다. 실제 장시간 WSS/마이크와 provider 기반 한숨·울음 감지 증거는 남아 있다.</p></div><div><b>산출물</b><p><code>app.turns.provider_events</code>, <code>TurnRecord.provider_events</code>, voice route 이벤트 sanitizing/taxonomy, <code>ReviewNonverbalEvent</code>, <code>SessionReview</code> 칩 UI</p></div><div><b>검증</b><p><code>pytest -p no:cacheprovider app/test_voice_ws.py app/test_session_turn_persistence.py app/test_voice_service.py -q</code> 45 passed; provider_events DB insert placeholder/args, WS raw key drop, STT+control event merge, internal taxonomy, client turn 미혼입, review 파생 칩 raw provider 미노출 검증. <code>npm run check:api-types</code>, <code>npm run typecheck</code>, <code>npm run build</code>, <code>session-review.spec.ts</code> 3 passed.</p></div></div>
<divclass="task-row"><div><spanclass="task-status s-doing">CHECK</span></div><div><b>s2s 2차 PoC 판단</b><p>캐스케이드 1차 원칙과 s2s 유지/폐기 증거 기준을 문서화했다. 채택 여부 결정은 윤찬 DECIDE로 유지한다.</p></div><div><b>산출물</b><p><code>docs/decisions/voice-s2s-poc.md</code></p></div><div><b>검증</b><p>transcript, safety, audit, latency, failure, privacy 기준 명시</p></div></div>
<divclass="task-row"><div><spanclass="task-status s-risk">DECIDE</span></div><div><b>s2s 2차 PoC 판단</b><p>캐스케이드 1차 원칙과 s2s 유지/폐기 증거 기준을 문서화했다. 채택 여부 결정은 윤찬 DECIDE로 유지한다.</p></div><div><b>산출물</b><p><code>docs/decisions/voice-s2s-poc.md</code></p></div><div><b>검증</b><p>transcript, safety, audit, latency, failure, privacy 기준 명시</p></div></div>
</div>
</div>
</div>
</div>
@ -797,7 +829,7 @@
<divclass="detail-head"><div><h3>Phase 3 교육용 파일럿 / 효과성 상세</h3><p>IRB 승인 게이트가 아니라 교육 활용 파일럿과 데이터/개인정보 게이트로 관리한다.</p></div><divclass="mini-metrics"><span>주차: 18-20+</span><span>표본: 20명</span><span>first measure</span></div></div>
<divclass="detail-head"><div><h3>Phase 3 교육용 파일럿 / 효과성 상세</h3><p>IRB 승인 게이트가 아니라 교육 활용 파일럿과 데이터/개인정보 게이트로 관리한다.</p></div><divclass="mini-metrics"><span>주차: 18-20+</span><span>표본: 20명</span><span>first measure</span></div></div>
<divclass="task-list">
<divclass="task-list">
<divclass="task-row"><div><spanclass="task-status s-plan">GATE</span></div><div><b>20명 교육용 파일럿 운영</b><p>20명 파일럿을 실행하기 위한 roster, consent, session completion evidence gate를 문서화했다. checker는 CSV enum(<code>withdrawal_state</code>, <code>completion_state</code>, <code>timepoint</code>, withdrawal <code>scope/status</code>)을 검증한다. 실제 참여자 완주 증거는 아직 필요하다.</p></div><div><b>산출물</b><p><code>docs/phase3/pilot-runbook.md</code>, evidence root layout, operator gates, <code>scripts/check-phase3-artifacts.py</code></p></div><div><b>검증</b><p><code>python scripts/check-phase3-artifacts.py --help</code>, <code>pytest app/test_phase3_artifact_checker.py -q</code> 5 passed. 실제 evidence root validation은 파일럿 시점에 실행.</p></div></div>
<divclass="task-row"><div><spanclass="task-status s-plan">GATE</span></div><div><b>20명 교육용 파일럿 운영</b><p>20명 파일럿을 실행하기 위한 roster, consent, session completion evidence gate를 문서화했다. checker는 CSV enum(<code>withdrawal_state</code>, <code>completion_state</code>, <code>timepoint</code>, withdrawal <code>scope/status</code>)을 검증한다. 실제 참여자 완주 증거는 아직 필요하다.</p></div><div><b>산출물</b><p><code>docs/phase3/pilot-runbook.md</code>, evidence root layout, operator gates, <code>scripts/check-phase3-artifacts.py</code></p></div><div><b>검증</b><p><code>python scripts/check-phase3-artifacts.py --help</code>, <code>pytest app/test_phase3_artifact_checker.py -q</code> 5 passed. 실제 evidence root validation은 파일럿 시점에 실행.</p></div></div>
<divclass="task-row"><div><spanclass="task-status s-done">DONE</span></div><div><b>재귀학습 데이터셋 1차 산출</b><p>masked-text JSONL dry-run exporter, PII scan, κ/ICC 계산, approved export 차단 게이트를 구현했다. 이번 패스에서 artifact checker도 approved manifest false-positive를 막도록 강화했다. 실제 approved export와 데이터 steward 검수는 남아 있다.</p></div><div><b>산출물</b><p><code>scripts/export-recursive-dataset.py</code>, <code>app.services.dataset_export</code>, <code>docs/phase3/dataset-export-manifest.md</code>, <code>app/test_phase3_artifact_checker.py</code></p></div><div><b>검증</b><p><code>pytest app/test_dataset_export.py app/test_phase3_artifact_checker.py -q</code> 10 passed, DB dry-run 5 rows → <code>technical_dry_run</code> manifest + sha256 생성, approved manifest는 PII pass·κ/ICC·withdrawn exclusion·consent scope·file sha256을 요구.</p></div></div>
<divclass="task-row"><div><spanclass="task-status s-done">DONE</span></div><div><b>재귀학습 데이터셋 1차 산출</b><p>masked-text JSONL dry-run exporter, PII scan, κ/ICC 계산, approved export 차단 게이트를 구현했다. 이번 패스에서 artifact checker도 approved manifest false-positive를 막도록 강화했다. 실제 approved export와 데이터 steward 검수는 남아 있다.</p></div><div><b>산출물</b><p><code>scripts/export-recursive-dataset.py</code>, <code>app.services.dataset_export</code>, <code>docs/phase3/dataset-export-manifest.md</code>, <code>app/test_phase3_artifact_checker.py</code></p></div><div><b>검증</b><p><code>pytest app/test_dataset_export.py app/test_phase3_artifact_checker.py -q</code> 10 passed, DB dry-run 5 rows → <code>technical_dry_run</code> manifest + sha256 생성, approved manifest는 PII pass·κ/ICC·withdrawn exclusion·consent scope·file sha256을 요구.</p></div></div>
<divclass="task-row"><div><spanclass="task-status s-plan">GATE</span></div><div><b>개인정보/동의 감사</b><p>동의, 철회, privacy audit, 해외 전송 고지 evidence checklist를 만들었다. 실제 법무/개인정보 검토 서명은 파일럿 전 gate로 남아 있다.</p></div><div><b>산출물</b><p><code>docs/phase3/privacy-consent-audit.md</code>, withdrawal log contract</p></div><div><b>검증</b><p>동의 철회 후 session/export 제외 evidence를 checker 입력으로 요구</p></div></div>
<divclass="task-row"><div><spanclass="task-status s-plan">GATE</span></div><div><b>개인정보/동의 감사</b><p>동의, 철회, privacy audit, 해외 전송 고지 evidence checklist를 만들었다. 실제 법무/개인정보 검토 서명은 파일럿 전 gate로 남아 있다.</p></div><div><b>산출물</b><p><code>docs/phase3/privacy-consent-audit.md</code>, withdrawal log contract</p></div><div><b>검증</b><p>동의 철회 후 session/export 제외 evidence를 checker 입력으로 요구</p></div></div>
</div>
</div>
@ -809,14 +841,14 @@
<divclass="task-row"><div><spanclass="task-status s-done">DONE</span></div><div><b>공개/API 런타임 health</b><p>터널은 prod API 8001을 보고, API는 DB와 engine을 모두 확인한다. 로컬 dev API 8010도 같은 엔진/DB 경로로 정상이다.</p></div><div><b>산출물</b><p><code>environment:"prod"</code>, <code>environment:"dev"</code>, <code>db:true</code>, <code>engine:true</code>, <code>engine_mode:"claude_cli"</code></p></div><div><b>검증</b><p><code>https://api-vignette.chanpaca.net/health</code>, <code>http://127.0.0.1:8001/health</code>, <code>http://127.0.0.1:8010/health</code> 모두 OK</p></div></div>
<divclass="task-row"><div><spanclass="task-status s-done">DONE</span></div><div><b>공개/API 런타임 health</b><p>터널은 prod API 8001을 보고, API는 DB와 engine을 모두 확인한다. 로컬 dev API 8010도 같은 엔진/DB 경로로 정상이다.</p></div><div><b>산출물</b><p><code>environment:"prod"</code>, <code>environment:"dev"</code>, <code>db:true</code>, <code>engine:true</code>, <code>engine_mode:"claude_cli"</code></p></div><div><b>검증</b><p><code>https://api-vignette.chanpaca.net/health</code>, <code>http://127.0.0.1:8001/health</code>, <code>http://127.0.0.1:8010/health</code> 모두 OK</p></div></div>
<divclass="task-row"><div><spanclass="task-status s-done">DONE</span></div><div><b>Tailscale Tailnet 런타임</b><p>로컬 web/api/engine을 Tailscale Serve로 묶어 같은 Tailnet PC·모바일에서 HTTPS로 접근한다. Vite proxy와 API 포트가 어긋나지 않도록 <code>dev-up.ps1</code>에서 <code>VITE_API_PROXY_TARGET</code>을 <code>ApiPort</code>와 동기화한다. Tailnet dev-origin에서는 공개 OAuth callback으로 세션이 섞이지 않게 dev-login을 기본 검증 경로로 둔다.</p></div><div><b>산출물</b><p><code>https://alpaca-home.taile93291.ts.net</code>, <code>scripts/start-tailscale-runtime.ps1</code>, <code>VITE_ALLOWED_HOSTS</code>, <code>vite.config.ts</code>, <code>auth.py</code></p></div><div><b>검증</b><p><code>/login</code> 200, <code>/api/auth/config</code> 200, <code>redirect_uri=https://api-vignette.chanpaca.net/auth/callback</code>, <code>dev_login_enabled:true</code>. Vite host-header block 재현 안 됨.</p></div></div>
<divclass="task-row"><div><spanclass="task-status s-done">DONE</span></div><div><b>Tailscale Tailnet 런타임</b><p>로컬 web/api/engine을 Tailscale Serve로 묶어 같은 Tailnet PC·모바일에서 HTTPS로 접근한다. Vite proxy와 API 포트가 어긋나지 않도록 <code>dev-up.ps1</code>에서 <code>VITE_API_PROXY_TARGET</code>을 <code>ApiPort</code>와 동기화한다. Tailnet dev-origin에서는 공개 OAuth callback으로 세션이 섞이지 않게 dev-login을 기본 검증 경로로 둔다.</p></div><div><b>산출물</b><p><code>https://alpaca-home.taile93291.ts.net</code>, <code>scripts/start-tailscale-runtime.ps1</code>, <code>VITE_ALLOWED_HOSTS</code>, <code>vite.config.ts</code>, <code>auth.py</code></p></div><div><b>검증</b><p><code>/login</code> 200, <code>/api/auth/config</code> 200, <code>redirect_uri=https://api-vignette.chanpaca.net/auth/callback</code>, <code>dev_login_enabled:true</code>. Vite host-header block 재현 안 됨.</p></div></div>
<divclass="task-row"><div><spanclass="task-status s-done">DONE</span></div><div><b>Docker compose 패키징 smoke</b><p>더미 운영 env 기준 fresh compose 기동을 통과했다. API 컨테이너 import 실패, 구현 없는 RAG 사이드카, 로컬 <code>apps/api/.env</code> 이미지 혼입, startup owner-DDL, 빈 <code>admin_engine_config</code> 행 때문에 prod가 죽던 경로를 수정했다. 루트 계산은 <code>app.paths.repo_root()</code>로 모았고, backend requirements는 exact pin으로 고정했다.</p></div><div><b>운영 주의</b><p>패키징은 OK지만 실제 이식 전에는 배포지 <code>infra/.env</code>에 <code>APP_DB_PASSWORD</code>, OAuth client id/secret, <code>OPENAI_API_KEY</code>, <code>SESSION_SECRET</code> 등 소유자 비밀값을 채워야 한다. <code>data/kb</code> live-coaching source pack은 commit 또는 같은 경로 provision이 필요하다. 현재 로컬 env의 <code>ENGINE_MODE=claude_p</code>와 prod 금지 샘플 TTS 플래그는 운영값으로 쓰면 안 된다.</p></div><div><b>검증</b><p><code>docker compose -f infra/docker-compose.yml config --quiet</code>, API/Web image build, API image <code>.env*</code> 미포함, prod import smoke, fresh <code>docker compose up -d --build</code> → <code>/api/health</code> 200, preflight <code>OK requirements_pinned/live_coach_kb/env_file</code>.</p></div></div>
<divclass="task-row"><div><spanclass="task-status s-done">DONE</span></div><div><b>Docker compose 패키징 smoke</b><p>더미 운영 env 기준 fresh compose 기동을 통과했다. API 컨테이너 import 실패, 구현 없는 RAG 사이드카, 로컬 <code>apps/api/.env</code> 이미지 혼입, startup owner-DDL, 빈 <code>admin_engine_config</code> 행 때문에 prod가 죽던 경로를 수정했다. 루트 계산은 <code>app.paths.repo_root()</code>로 모았고, backend requirements는 exact pin으로 고정했다.</p></div><div><b>운영 주의</b><p>패키징은 OK지만 실제 이식 전에는 배포지 <code>infra/.env</code>에 <code>APP_DB_PASSWORD</code>, OAuth client id/secret, <code>OPENAI_API_KEY</code>, <code>SESSION_SECRET</code> 등 소유자 비밀값을 채워야 한다. <code>data/kb</code> live-coaching source pack은 commit 또는 같은 경로 provision이 필요하다. 현재 로컬 env의 <code>ENGINE_MODE=claude_p</code>와 prod 금지 샘플 TTS 플래그는 운영값으로 쓰면 안 된다.</p></div><div><b>검증</b><p><code>docker compose -f infra/docker-compose.yml config --quiet</code>, API/Web image build, API image <code>.env*</code> 미포함, prod import smoke, fresh <code>docker compose up -d --build</code> → <code>/api/health</code> 200, preflight <code>OK requirements_pinned/live_coach_kb/env_file</code>.</p></div></div>
<divclass="task-row"><div><spanclass="task-status s-doing">BLOCKED</span></div><div><b>공개 vnet.18ka.net 런타임</b><p><code>vnet.18ka.net</code>과 <code>api-vnet.18ka.net</code>을 정식 공개 도메인으로 허용하도록 코드 경로는 보강했다.</p></div><div><b>산출물</b><p><code>FRONTEND_ORIGIN_MAP</code>, 공개 host별 API 매핑, <code>scripts/start-public-runtime.ps1</code> vnet ingress 옵션</p></div><div><b>블로커</b><p>현재 DNS 미해결, Cloudflare credential의 18ka.net zone 권한 불일치, Google OAuth redirect URI 등록 필요.</p></div></div>
<divclass="task-row"><div><spanclass="task-status s-risk">BLOCKED</span></div><div><b>공개 vnet.18ka.net 런타임</b><p><code>vnet.18ka.net</code>과 <code>api-vnet.18ka.net</code>을 정식 공개 도메인으로 허용하도록 코드 경로는 보강했다.</p></div><div><b>산출물</b><p><code>FRONTEND_ORIGIN_MAP</code>, 공개 host별 API 매핑, <code>scripts/start-public-runtime.ps1</code> vnet ingress 옵션</p></div><div><b>블로커</b><p>현재 DNS 미해결, Cloudflare credential의 18ka.net zone 권한 불일치, Google OAuth redirect URI 등록 필요.</p></div></div>
<divclass="task-row"><div><spanclass="task-status s-done">DONE</span></div><div><b>서버 정리 상태</b><p>운영 API와 로컬/Tailnet 테스트 API를 구분해 유지한다.</p></div><div><b>산출물</b><p>8001 prod API, 8000 local API, 8010 Tailnet API, 5173 web, 9099 engine, 20241 cloudflared</p></div><div><b>검증</b><p><code>Get-NetTCPConnection</code> 대상 포트 확인</p></div></div>
<divclass="task-row"><div><spanclass="task-status s-done">DONE</span></div><div><b>서버 정리 상태</b><p>운영 API와 로컬/Tailnet 테스트 API를 구분해 유지한다.</p></div><div><b>산출물</b><p>8001 prod API, 8000 local API, 8010 Tailnet API, 5173 web, 9099 engine, 20241 cloudflared</p></div><div><b>검증</b><p><code>Get-NetTCPConnection</code> 대상 포트 확인</p></div></div>
<divclass="task-row"><div><spanclass="task-status s-doing">CHECK</span></div><div><b>재부팅 후 런타임 재기동</b><p>engine gateway, prod API, cloudflared를 health-check하고 복구하는 watchdog과 Scheduled Task installer를 추가했다. 아직 DNS가 없는 <code>api-vnet.18ka.net</code>은 기본 watchdog 검사에서 제외해 불필요한 restart loop를 막고, 실제 개통 후 <code>-AdditionalPublicHealthUrls</code>로 명시 추가한다. 실제 재부팅 후 public <code>/turn</code> smoke는 남아 있다.</p></div><div><b>산출물</b><p><code>watch-public-runtime.ps1</code>, <code>install-public-runtime-task.ps1</code>, ops runbook</p></div><div><b>검증</b><p>PowerShell parser OK, check-only watchdog healthy: engine/api/web-preview/cloudflared/public-api</p></div></div>
<divclass="task-row"><div><spanclass="task-status s-doing">CHECK</span></div><div><b>재부팅 후 런타임 재기동</b><p>engine gateway, prod API, cloudflared를 health-check하고 복구하는 watchdog과 Scheduled Task installer를 추가했다. 아직 DNS가 없는 <code>api-vnet.18ka.net</code>은 기본 watchdog 검사에서 제외해 불필요한 restart loop를 막고, 실제 개통 후 <code>-AdditionalPublicHealthUrls</code>로 명시 추가한다. 실제 재부팅 후 public <code>/turn</code> smoke는 남아 있다.</p></div><div><b>산출물</b><p><code>watch-public-runtime.ps1</code>, <code>install-public-runtime-task.ps1</code>, ops runbook</p></div><div><b>검증</b><p>PowerShell parser OK, check-only watchdog healthy: engine/api/web-preview/cloudflared/public-api</p></div></div>
<divclass="task-row"><div><spanclass="task-status s-done">DONE</span></div><div><b>레이아웃 감사팀 / 전면 레이아웃 개선</b><p>서브에이전트 전담으로 1차 overflow/clipping 수리 후 2차 전면 재설계를 진행했고, 이번 라운드에서 7개 권장 너비 시각 수용까지 닫았다. 세션, 학습자 홈/리뷰, 교수 콘솔, 관리자, 설정 화면을 작업 화면 기준으로 재배치했고, 과한 빈칸·튀어나감·버튼/텍스트 잘림·모바일 테이블 사용성을 줄였다. 엄격한 시각 게이트 + 화면별 적대적 시각 재검수에서 7개 화면 모두 accept(critical/major 0, 회귀 0)를 받아 DONE으로 닫는다.</p></div><div><b>산출물</b><p><code>docs/ops/layout-redesign-handoff-2026-06-26.md</code>, page-by-page redesign patches, <code>apps/web/e2e/layout-visual-gate.spec.ts</code></p></div><div><b>검증</b><p><code>npm run typecheck</code> OK, 엄격 시각 게이트 7 passed, layout focused E2E desktop/mobile 54 passed, 적대적 시각 재검수 7/7 accept·회귀 0.</p></div></div>
<divclass="task-row"><div><spanclass="task-status s-done">DONE</span></div><div><b>레이아웃 감사팀 / 전면 레이아웃 개선</b><p>서브에이전트 전담으로 1차 overflow/clipping 수리 후 2차 전면 재설계를 진행했고, 이번 라운드에서 빈 회기리뷰까지 포함해 7개 권장 너비 시각 수용을 닫았다. 세션, 학습자 홈/리뷰, 교수 콘솔, 관리자, 설정 화면을 작업 화면 기준으로 재배치했고, 과한 빈칸·튀어나감·버튼/텍스트 잘림·모바일 테이블 사용성을 줄였다. 엄격한 시각 게이트 + 화면별 적대적 시각 재검수에서 critical/major 0, 회귀 0을 받아 DONE으로 닫는다.</p></div><div><b>산출물</b><p><code>docs/ops/layout-redesign-handoff-2026-06-26.md</code>, page-by-page redesign patches, <code>apps/web/e2e/layout-visual-gate.spec.ts</code></p></div><div><b>검증</b><p><code>npm run typecheck</code> OK, 현재 엄격 시각 게이트 9 passed, layout focused E2E desktop/mobile 54 passed, 적대적 시각 재검수 7/7 accept·회귀 0.</p></div></div>
<divclass="task-row"><div><spanclass="task-status s-done">DONE</span></div><div><b>엄격한 레이아웃 시각 게이트 구축 · 병렬 수정 · 적대적 재검수</b><p>핸드오프가 요구한 7개 권장 너비(390/720/861/900/1024/1280/1440) 시각 수용을 자동 게이트로 고정했다. 7개 재설계 화면을 각 너비에서 렌더링해 가로 overflow 0, 컨트롤 하드클립/텍스트클립 0을 강제하고 화면당 7장 총 49장 풀페이지 스크린샷을 남긴다. 화면별 1에이전트 병렬 시각 리뷰로 49장 직접 판독 → 6개 병렬 수정 에이전트(파일 비중첩)가 결함 수정 → 화면별 적대적 재검수로 해소·무회귀 확정.</p></div><div><b>산출물</b><p><code>apps/web/e2e/layout-visual-gate.spec.ts</code>, <code>node_modules/.tmp/layout-gate/*.png</code>49장, 2× workflow(review/verify)·6× fix agent</p></div><div><b>검증</b><p>게이트 7 passed(재스크린샷). 해소된 핵심 결함: 학습자홈 1280/1440 3열 가운데 컬럼 붕괴(critical → 2열 분기 상향 + word-break:keep-all), 교수 'API 404' raw 배너 제거, 리뷰 빈상태 위계 역전, 관리자 스켈레톤, 설정 와이드 좌측 데드존 제거, 세션 모바일 44px 터치타깃. 적대적 재검수 7/7 accept, regression 0.</p></div></div>
<divclass="task-row"><div><spanclass="task-status s-done">DONE</span></div><div><b>엄격한 레이아웃 시각 게이트 구축 · 병렬 수정 · 적대적 재검수</b><p>핸드오프가 요구한 7개 권장 너비(390/720/861/900/1024/1280/1440) 시각 수용을 자동 게이트로 고정했다. 현재는 빈 회기리뷰 전용 화면까지 9개 화면을 각 너비에서 렌더링해 가로 overflow 0, 컨트롤 하드클립/텍스트클립 0을 강제하고 화면당 7장 총 63장 풀페이지 스크린샷을 남긴다. 화면별 1에이전트 병렬 시각 리뷰로 49장 직접 판독 → 6개 병렬 수정 에이전트(파일 비중첩)가 결함 수정 → 화면별 적대적 재검수로 해소·무회귀 확정.</p></div><div><b>산출물</b><p><code>apps/web/e2e/layout-visual-gate.spec.ts</code>, <code>node_modules/.tmp/layout-gate/*.png</code>63장, 2× workflow(review/verify)·6× fix agent</p></div><div><b>검증</b><p>현재 게이트 9 passed(재스크린샷). 해소된 핵심 결함: 학습자홈 1280/1440 3열 가운데 컬럼 붕괴(critical → 2열 분기 상향 + word-break:keep-all), 교수 'API 404' raw 배너 제거, 리뷰 빈상태 위계 역전과 1280 sparse third-column, 관리자 스켈레톤, 설정 와이드 좌측 데드존 제거, 세션 모바일 44px 터치타깃. 적대적 재검수 7/7 accept, regression 0.</p></div></div>
<divclass="task-row"><div><spanclass="task-status s-done">DONE</span></div><div><b>레이아웃 cosmetic minor 폴리시 일괄 처리</b><p>적대적 재검수 잔여 cosmetic minor를 5개 병렬 폴리시 에이전트로 처리했다. 학습자홈 헬퍼문구 고아 글자, 세션 1024 일시정지 아이콘 정리 + 보내기 버튼 대비 강화, 설정 2x2 칩 행 높이 균일, 교수 검토 큐 카드 갭 제거, 관리자 2열 높이 동기화를 적용했다.</p></div><div><b>산출물</b><p>page-by-page polish patches(LearnerHome/Session/Settings/Professor/Admin)</p></div><div><b>검증</b><p><code>npm run typecheck</code> OK, 시각 게이트 7 passed, 레이아웃 E2E desktop/mobile 54 passed, session-layout 8/8 — 회귀 0.</p></div></div>
<divclass="task-row"><div><spanclass="task-status s-done">DONE</span></div><div><b>레이아웃 cosmetic minor 폴리시 일괄 처리</b><p>적대적 재검수 잔여 cosmetic minor를 5개 병렬 폴리시 에이전트로 처리했다. 학습자홈 헬퍼문구 고아 글자, 세션 1024 일시정지 아이콘 정리 + 보내기 버튼 대비 강화, 설정 2x2 칩 행 높이 균일, 교수 검토 큐 카드 갭 제거, 관리자 2열 높이 동기화를 적용했다.</p></div><div><b>산출물</b><p>page-by-page polish patches(LearnerHome/Session/Settings/Professor/Admin)</p></div><div><b>검증</b><p><code>npm run typecheck</code> OK, 시각 게이트 7 passed, 레이아웃 E2E desktop/mobile 54 passed, session-layout 8/8 — 회귀 0.</p></div></div>
<divclass="task-row"><div><spanclass="task-status s-done">DONE</span></div><div><b>회기 아카이브 저장/복원 API</b><p><code>/learn/history</code>의 <code>보관됨</code>을 실제 학습자별 저장 상태로 연결했다. 종료 회기는 <code>POST /sessions/{id}/archive</code>/<code>restore</code>로 보관·복원하고, <code>app.session_archive_state</code>는 보기 상태만 저장한다. 진행 중 회기는 보관하지 못하며, 보관은 transcript/review/share/audit evidence를 삭제하지 않는다.</p></div><div><b>산출물</b><p><code>SessionArchiveResponse</code>, <code>LearnerSessionSummary.archived</code>, <code>LearnerDashboardOverview.archived_sessions</code>, <code>session_archive_state</code> RLS, <code>LearnerHome</code> 보관/복원 버튼</p></div><div><b>검증</b><p><code>python -B -m pytest app/ -q</code> 178 passed, <code>python -B -m pytest engine_gateway/ -q</code> 11 passed, <code>npm run check:api-types</code>, <code>npm run typecheck</code>, <code>npm run build</code>, <code>npx playwright test e2e/learner.spec.ts --project=chromium-desktop --workers=1</code> 6 passed.</p></div></div>
<divclass="task-row"><div><spanclass="task-status s-done">DONE</span></div><div><b>회기 아카이브 저장/복원 API</b><p><code>/learn/history</code>의 <code>보관됨</code>을 실제 학습자별 저장 상태로 연결했다. 종료 회기는 <code>POST /sessions/{id}/archive</code>/<code>restore</code>로 보관·복원하고, <code>app.session_archive_state</code>는 보기 상태만 저장한다. 진행 중 회기는 보관하지 못하며, 보관은 transcript/review/share/audit evidence를 삭제하지 않는다.</p></div><div><b>산출물</b><p><code>SessionArchiveResponse</code>, <code>LearnerSessionSummary.archived</code>, <code>LearnerDashboardOverview.archived_sessions</code>, <code>session_archive_state</code> RLS, <code>LearnerHome</code> 보관/복원 버튼</p></div><div><b>검증</b><p><code>python -B -m pytest app/ -q</code> 178 passed, <code>python -B -m pytest engine_gateway/ -q</code> 11 passed, <code>npm run check:api-types</code>, <code>npm run typecheck</code>, <code>npm run build</code>, <code>npx playwright test e2e/learner.spec.ts --project=chromium-desktop --workers=1</code> 6 passed.</p></div></div>
<divclass="task-row"><div><spanclass="task-status s-done">DONE</span></div><div><b>세션 종료 UX와 다크 테마 SSOT</b><p>세션 하단의 드래그형 종료 컴포넌트를 명시 확인 다이얼로그로 바꿔 모바일 오발동과 제스처 실패를 줄였다. Topbar/Settings의 theme 저장·초기화는 <code>lib/theme.ts</code> 단일 경로로 합치고, 저장값이 없으면 dark 기본값을 앱 부팅 전에 적용한다. API 기본 preference <code>system</code>은 Settings에서 light로 오해하지 않고 현재 초기 테마를 따른다. P1 실제 세션 기본 아바타는 사용자 제공 PSD 기반 <code>seoyeon-live2d-psd-v2</code>이며, sad 표정에서 울상 눈썹·우는 입·눈물 파츠를 합성한다. active session 스테이지 상태 배지는 아바타와 겹치지 않는 3행 구조로 정리했다.</p></div><div><b>산출물</b><p><code>Session.tsx</code>, <code>RasterBust.tsx</code>, <code>AvatarPreview.tsx</code>, <code>session.css</code>, <code>apps/web/public/avatar/seoyeon-live2d-psd-v2/parts/*.png</code>, <code>docs/avatar-art/seoyeon/live2d-psd-v2/</code>, <code>avatar-expression.spec.ts</code>, <code>session-layout.spec.ts</code></p></div><div><b>검증</b><p>PSD 파츠 추출 53개. <code>preview-sad.png</code>에서 눈물/우는 입/울상 눈썹 확인. 기존 시각 회귀 게이트는 후속 전체 검증에서 재실행한다.</p></div></div>
<divclass="task-row"><div><spanclass="task-status s-done">DONE</span></div><div><b>세션 종료 UX와 다크 테마 SSOT</b><p>세션 하단의 드래그형 종료 컴포넌트를 명시 확인 다이얼로그로 바꿔 모바일 오발동과 제스처 실패를 줄였다. Topbar/Settings의 theme 저장·초기화는 <code>lib/theme.ts</code> 단일 경로로 합치고, 저장값이 없으면 dark 기본값을 앱 부팅 전에 적용한다. API 기본 preference <code>system</code>은 Settings에서 light로 오해하지 않고 현재 초기 테마를 따른다. P1 실제 세션 기본 아바타는 사용자 제공 PSD 기반 <code>seoyeon-live2d-psd-v2</code>이며, sad 표정에서 울상 눈썹·우는 입·눈물 파츠를 합성한다. active session 스테이지 상태 배지는 아바타와 겹치지 않는 3행 구조로 정리했다.</p></div><div><b>산출물</b><p><code>Session.tsx</code>, <code>RasterBust.tsx</code>, <code>AvatarPreview.tsx</code>, <code>session.css</code>, <code>apps/web/public/avatar/seoyeon-live2d-psd-v2/parts/*.png</code>, <code>docs/avatar-art/seoyeon/live2d-psd-v2/</code>, <code>avatar-expression.spec.ts</code>, <code>session-layout.spec.ts</code></p></div><div><b>검증</b><p>PSD 파츠 추출 53개. <code>preview-sad.png</code>에서 눈물/우는 입/울상 눈썹 확인. 시각 회귀 묶음은 <code>npm run typecheck</code>, <code>npm run build</code>, Playwright 36 passed로 재검증했다.</p></div></div>
<divclass="task-row"><div><spanclass="task-status s-done">DONE</span></div><div><b>잔여(비차단) — 공용 셸 단일 항목 → 처리</b><p>축소 사이드바 세로 구분선이 본문 전체 높이까지 닿지 않던 건을 <code>components/shell/shell.css</code>에서 처리했다. <code>.vg-nav border-right</code> 제거 후 <code>.vg-shell__body</code> 컨테이너 배경 하어라인으로 본문 그리드 전체 높이 구분선을 그리고, 그리드·구분선 폭을 <code>--nav-cur</code>로 동기화. learner-home 로딩 스켈레톤 밀도도 실제 카드 구조 모사로 보강했다.</p></div><div><b>판정</b><p>구현 완료. 검증: <code>npm run typecheck</code> PASS + <code>vite build</code> PASS. 전 페이지 시각 회귀 게이트는 web+api+DB 스택으로 이 워크스테이션 미실행 — 스택 가용 시 1회 시각 확인 권장.</p></div></div>
<divclass="task-row"><div><spanclass="task-status s-done">DONE</span></div><div><b>잔여(비차단) — 공용 셸 단일 항목 → 처리</b><p>축소 사이드바 세로 구분선이 본문 전체 높이까지 닿지 않던 건을 <code>components/shell/shell.css</code>에서 처리했다. <code>.vg-nav border-right</code> 제거 후 <code>.vg-shell__body</code> 컨테이너 배경 하어라인으로 본문 그리드 전체 높이 구분선을 그리고, 그리드·구분선 폭을 <code>--nav-cur</code>로 동기화. learner-home 로딩 스켈레톤 밀도도 실제 카드 구조 모사로 보강했다.</p></div><div><b>판정</b><p>구현 완료. 검증: <code>npm run typecheck</code> PASS + <code>vite build</code> PASS. 전 페이지 시각 회귀 게이트는 web+api+DB 스택으로 이 워크스테이션 미실행 — 스택 가용 시 1회 시각 확인 권장.</p></div></div>
</div>
</div>
<divclass="tab-subhead"><h4>검증 실행 상세 표</h4><p>요약하지 않고 실행 범위, 명령, 결과를 행 단위로 남긴다.</p></div>
<divclass="tab-subhead"><h4>검증 실행 상세 표</h4><p>요약하지 않고 실행 범위, 명령, 결과를 행 단위로 남긴다.</p></div>
@ -829,29 +861,40 @@
<tr><td>Contract SSOT aggregate DTO</td><td><code>npm run typecheck</code> / <code>npm run build</code> / <code>session persistence</code> / <code>learner + session-review + teacher + layout gate</code></td><td>Passed; learner sessions, session review/worksheet, teacher dashboard, session start/detail DTOs now use generated <code>ApiSchema</code> aliases with UI fallback. Stage responses are OpenAPI enum unions. Focused E2E 32 passed. <code>.github/workflows/api-contract.yml</code> now runs <code>npm run check:api-types</code> on API/Web contract changes.</td></tr>
<tr><td>Contract SSOT aggregate DTO</td><td><code>npm run typecheck</code> / <code>npm run build</code> / <code>session persistence</code> / <code>learner + session-review + teacher + layout gate</code></td><td>Passed; learner sessions, session review/worksheet, teacher dashboard, session start/detail DTOs now use generated <code>ApiSchema</code> aliases with UI fallback. Stage responses are OpenAPI enum unions. Focused E2E 32 passed. <code>.github/workflows/api-contract.yml</code> now runs <code>npm run check:api-types</code> on API/Web contract changes.</td></tr>
<tr><td>SEO/GEO share cards</td><td><code>pytest app/test_session_share.py app/test_session_turn_persistence.py -q</code> / <code>npm run generate:api-types</code> / <code>npm run typecheck</code></td><td>21 passed; session share creates hashed-token public unfurl payload without raw transcript, revoked token returns 404, OpenAPI generated share DTOs, review screen share button typechecks. Static <code>robots.txt</code>/<code>sitemap.xml</code>/<code>llms.txt</code> added.</td></tr>
<tr><td>SEO/GEO share cards</td><td><code>pytest app/test_session_share.py app/test_session_turn_persistence.py -q</code> / <code>npm run generate:api-types</code> / <code>npm run typecheck</code></td><td>21 passed; session share creates hashed-token public unfurl payload without raw transcript, revoked token returns 404, OpenAPI generated share DTOs, review screen share button typechecks. Static <code>robots.txt</code>/<code>sitemap.xml</code>/<code>llms.txt</code> added.</td></tr>
<tr><td>X2 evaluator model routing</td><td><code>python -m pytest app/test_evaluator_model_routing.py app/test_runtime_policy.py -q</code></td><td>23 passed; fast/deep evaluator model override and blank-default routing covered</td></tr>
<tr><td>X2 evaluator routing/cache/cost trend</td><td><code>python -B -m pytest -p no:cacheprovider app/test_usage_report.py app/test_evaluator_model_routing.py app/test_runtime_policy.py app/test_admin_ops.py -q</code> / <code>scripts/report-ai-usage.py</code> sample CLI / prior web gates</td><td>42 backend passed + CLI sample OK; fast/deep evaluator model override, semantic cache hit/miss, admin usage budget/fail-closed, cache hit-rate observability, daily cost trend, and provider/model cost report schema covered. Prior API typecheck/build/admin E2E evidence remains valid because the API response shape was not changed.</td></tr>
<tr><td>H3 raw source isolation</td><td><code>python -B -m pytest -p no:cacheprovider app/test_persona_review.py app/test_live_coach_sources.py -q</code></td><td>38 passed; persona source registration records raw hash-only artifacts separately from sanitized evaluator-only RAG chunks, and <code>rag.index_document()</code> rejects <code>sensitivity=3</code>/raw-marker chunks before DB access.</td></tr>
<tr><td>H2 session review 2-column UX</td><td><code>npm run typecheck</code> / <code>npx playwright test e2e/session-review.spec.ts --project=chromium-desktop --workers=1</code> / <code>npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run --workers=1</code></td><td>Typecheck passed; 3 focused E2E passed; 9 visual gates passed. Filled learner review uses a left transcript rail and right work column at desktop widths while teacher/mobile layouts remain on existing rules.</td></tr>
<tr><td>M2 case memory</td><td><code>python -B -m pytest -p no:cacheprovider app/test_session_memory.py app/test_session_turn_persistence.py app/test_orchestrator_masking.py app/test_runtime_policy.py -q</code></td><td>67 passed; session end writes masked fallback digest, case_digest, rapport trajectory, alliance EWMA, conservative identity/agreement pinned_fact upserts, append-only pinned_fact_history on insert/change, explicit counseling-agreement withdrawal marks the existing non-locked fact as contradicted, and episodic writer stores only masked client-visible client turns into <code>app.turn_embedding</code> idempotently.</td></tr>
<tr><td>L1 session read-model split</td><td><code>python -B -m py_compile app/session_read_model.py app/routes/sessions.py</code> / <code>python -B -m pytest -p no:cacheprovider app/test_session_turn_persistence.py app/test_evaluation_persistence.py app/test_session_share.py app/test_learner_dashboard.py app/test_rbac_idor.py app/test_teacher_dashboard.py -q</code> / <code>npm run check:api-types</code> / <code>npm run typecheck</code></td><td>py_compile passed; focused backend 48 passed; OpenAPI generated API type check and web typecheck passed. <code>app/session_read_model.py</code> now owns session list/dashboard/detail/review/share DTOs and deterministic builders while <code>routes/sessions.py</code> keeps auth, RLS-backed reads, persistence, and route decorators.</td></tr>
<tr><td>M1 provider_events preservation</td><td><code>python -B -m pytest -p no:cacheprovider app/test_voice_ws.py app/test_session_turn_persistence.py app/test_voice_service.py -q</code></td><td>45 passed; voice control/STT provider events are allowlisted, size-limited, and tagged with internal <code>event_type</code>/<code>category</code> taxonomy into <code>app.turns.provider_events</code>. 인증된 review <code>nonverbal</code>은 정규화 taxonomy 일부만 파생 칩으로 노출하며 raw transcript/text/provider/source/raw type은 응답하지 않는다.</td></tr>
<tr><td>C1 case worksheet</td><td><code>python -m pytest app/test_session_turn_persistence.py app/test_rbac_idor.py -q</code> / <code>npm run check:api-types</code> / <code>npm run typecheck</code> / <code>npm run build</code> / local API smoke</td><td>25 passed; session review exposes transcript-grounded <code>caseWorksheet</code>, learner-edited <code>saved_by_learner</code> worksheet persists through <code>app.case_worksheet</code>, and evaluator-only turns stay hidden. Local smoke returned <code>saved_by_learner:local smoke saved worksheet</code>. Stage labels are normalized at response boundaries.</td></tr>
<tr><td>C1 case worksheet</td><td><code>python -m pytest app/test_session_turn_persistence.py app/test_rbac_idor.py -q</code> / <code>npm run check:api-types</code> / <code>npm run typecheck</code> / <code>npm run build</code> / local API smoke</td><td>25 passed; session review exposes transcript-grounded <code>caseWorksheet</code>, learner-edited <code>saved_by_learner</code> worksheet persists through <code>app.case_worksheet</code>, and evaluator-only turns stay hidden. Local smoke returned <code>saved_by_learner:local smoke saved worksheet</code>. Stage labels are normalized at response boundaries.</td></tr>
<tr><td>Repo source pack sync</td><td><code>python -B -m pytest -p no:cacheprovider app/test_live_coach_sources.py -q</code> / <code>py -3.11 scripts\sync-persona-sources.py --help</code></td><td>4 passed; dry-run reports next version without writes, apply bumps changed hash to latest+1, identical hash skips indexing, CLI exposes DB-backed dry-run/apply modes.</td></tr>
<tr><td>P1 cost telemetry</td><td><code>app.test_session_turn_persistence.SessionTurnPersistenceTest</code></td><td>Passed; generate/stream client AI turns store provider/model/tokens/cost</td></tr>
<tr><td>P1 cost telemetry</td><td><code>app.test_session_turn_persistence.SessionTurnPersistenceTest</code></td><td>Passed; generate/stream client AI turns store provider/model/tokens/cost</td></tr>
<tr><td>Engine stream contract</td><td><code>gateway token/done/error parsing</code></td><td>Passed; gateway error event does not create saved turns</td></tr>
<tr><td>Engine stream contract</td><td><code>node scripts/check-engine-gateway-contract.mjs --json</code> / <code>python -B -m pytest -p no:cacheprovider engine_gateway/test_gateway_model.py -q</code> / <code>python -B -m pytest -p no:cacheprovider engine_gateway/test_gateway_model.py app/test_orchestrator_masking.py app/test_session_turn_persistence.py -q</code></td><td>Node conformance runner OK, gateway contract 19 passed, L1 focused 50 passed; <code>EngineClient.stream_packets()</code> owns raw SSE decode, <code>EngineGatewaySseLineDecoder</code> parses token/done/error from the shared contract, keeps legacy token text compatibility, treats provider <code>[DONE]</code> as an ignored compatibility line rather than completion, rejects unknown gateway events, validates schema-backed <code>engine_gateway_contract.v1.json</code>, and gateway error event does not create saved turns.</td></tr>
<tr><td>Engine session reuse</td><td><code>engine_gateway.test_gateway_model</code></td><td>9 tests OK; live session_id reuse and ephemeral close fixed</td></tr>
<tr><td>Engine session reuse</td><td><code>engine_gateway.test_gateway_model</code></td><td>19 tests OK; shared engine contract, JSON Schema + golden fixture validation, Node.js artifact conformance runner, <code>GenerateResponse</code> response validation, SSE frames/decoder, live session_id reuse and ephemeral close fixed</td></tr>
<tr><td>P1 masking gate</td><td><code>app.test_orchestrator_masking</code></td><td>4 tests OK; phone/email/RRN absent from generate/stream payloads</td></tr>
<tr><td>P1/H4 masking gate</td><td><code>app/test_pii_masking_eval.py app/test_orchestrator_masking.py app/test_evaluation_persistence.py app/test_session_turn_persistence.py</code></td><td>41 passed; phone/email/RRN 및 한국어 NAME/ORG raw 값이 generate/stream/evaluator payload와 client <code>text_masked</code>에 남지 않음. Synthetic ko fixture 5/5 pass, entity recall 1.0, forbidden substring removal 1.0, unexpected entity violations 0.</td></tr>
<tr><td>Admin access delegation</td><td><code>python -m pytest app/test_auth_providers.py app/test_admin_ops.py -q</code> / <code>npm run typecheck</code> / <code>npm run check:api-types</code></td><td>32 backend passed; <code>admin_access</code> and <code>super_admin</code> are exposed through auth/admin DTOs. Super admins can grant/revoke admin page access; configured super admins stay protected. Frontend role switching uses <code>canAccessRole</code>.</td></tr>
<tr><td>Admin access delegation</td><td><code>python -m pytest app/test_auth_providers.py app/test_admin_ops.py -q</code> / <code>npm run typecheck</code> / <code>npm run check:api-types</code></td><td>32 backend passed; <code>admin_access</code> and <code>super_admin</code> are exposed through auth/admin DTOs. Super admins can grant/revoke admin page access; configured super admins stay protected. Frontend role switching uses <code>canAccessRole</code>.</td></tr>
<tr><td>Admin usage persistence</td><td><code>python -m pytest app/test_admin_ops.py app/test_runtime_policy.py -q</code> / authenticated local public-API smoke</td><td>26 passed; <code>/admin/usage</code> returns 200 with <code>source=database</code>, <code>durable=true</code>. Cloudflare blocked raw Python public smoke with 1010, so app-level HTTP was verified against <code>127.0.0.1:8001</code> using the same prod process.</td></tr>
<tr><td>Admin usage persistence</td><td><code>python -m pytest app/test_admin_ops.py app/test_runtime_policy.py -q</code> / authenticated local public-API smoke</td><td>26 passed; <code>/admin/usage</code> returns 200 with <code>source=database</code>, <code>durable=true</code>. Cloudflare blocked raw Python public smoke with 1010, so app-level HTTP was verified against <code>127.0.0.1:8001</code> using the same prod process.</td></tr>
<tr><td>Synthetic health sampler</td><td><code>record_admin_health_sample()</code>, <code>record-admin-health-sample.py</code>, <code>install-health-sampler-task.ps1</code></td><td>Backend focused 31 passed. Python compile/help passed; PowerShell parser + <code>-PrintOnly</code> passed. Local one-shot appended 5 service rows to <code>app.admin_health_event</code>: status ok, engine_mode claude_cli. This remains sample history, not an SLA claim.</td></tr>
<tr><td>Health retention/rollup</td><td><code>admin_health_daily_rollup</code>, <code>maintain-admin-health-events.py</code>, <code>/admin/uptime</code></td><td>Backend focused 36 passed. Python compile/help passed. Local dry-run returned <code>rollup_event_count=0</code>, <code>prunable_event_count=0</code>. Uptime summary now combines raw samples with daily rollups when raw rows have been pruned; detailed events stay raw-only.</td></tr>
<tr><td>Ticket duplicate linking</td><td><code>support_ticket.fingerprint</code>, <code>parent_ticket_id</code>, <code>/admin/tickets</code></td><td>Backend focused 38 passed. API types, web typecheck, and build passed. Admin tickets E2E 2 passed after restarting local API 8000 on the new code. Duplicate handling is deterministic hint + admin manual parent link/unlink only; no automatic merge, escalation, issue, PR, or thread creation.</td></tr>
<tr><td>User support ticket self-view</td><td><code>app/test_user_support_tickets.py</code>, <code>app/test_admin_ops.py</code>, <code>app/test_runtime_policy.py</code>, <code>settings.spec.ts</code>, <code>admin.spec.ts --grep "operation tickets"</code></td><td>Backend 29 passed; <code>/users/support-tickets</code> filters by current <code>reporter_id</code> and excludes ticket body/reporter PII. API types, typecheck, build passed. Settings E2E 7 passed; admin operation ticket E2E 1 passed.</td></tr>
<tr><td>Postgres RLS/audit smoke checker</td><td><code>python scripts\check-postgres-rls-audit.py --help</code></td><td>py_compile/help/safe default OK; live DSN run still required for proof</td></tr>
<tr><td>Postgres RLS/audit smoke checker</td><td><code>python scripts\check-postgres-rls-audit.py --help</code></td><td>py_compile/help/safe default OK; live DSN run still required for proof</td></tr>
<tr><td>OAuth/BFF cookie security</td><td><code>app.test_auth_providers</code></td><td>20 tests OK; PKCE/state, cookie-bound signed state restart recovery, state cookie mismatch reject, opaque HttpOnly/Secure cookie, no browser tokens, SAML AuthnRequest/dev-only fixture ACS, dev/Tailnet OAuth guard, public-origin dev-login closed</td></tr>
<tr><td>OAuth/BFF cookie security</td><td><code>python -B -m pytest -p no:cacheprovider app/test_auth_providers.py -q</code></td><td>30 tests OK; PKCE/state, cookie-bound signed state restart recovery, state cookie mismatch reject, opaque HttpOnly/Secure cookie, no browser tokens, SAML AuthnRequest/dev-only fixture ACS, dev/Tailnet OAuth guard, public-origin dev-login closed</td></tr>
<tr><td>H4 consent gate</td><td><code>pytest app/test_auth_providers.py app/test_session_turn_persistence.py app/test_persona_review.py app/test_voice_ws.py -q</code></td><td>64 passed; learner consent accept/withdraw, non-learner/unaccepted reject, no-consent session start 403 <code>consent_required</code>, voice dev persona consent guard, frontend generated API contract updated</td></tr>
<tr><td>H4 consent gate</td><td><code>pytest app/test_auth_providers.py app/test_session_turn_persistence.py app/test_persona_review.py app/test_voice_ws.py -q</code></td><td>64 passed; learner consent accept/withdraw, non-learner/unaccepted reject, no-consent session start 403 <code>consent_required</code>, voice dev persona consent guard, frontend generated API contract updated</td></tr>
<tr><td>H1 pre/post evidence API + KPI export</td><td><code>app/test_user_support_tickets.py</code>, <code>app/test_runtime_policy.py</code>, <code>app/test_admin_ops.py</code>, <code>app/test_phase3_kpi_export.py</code>, <code>app/test_phase3_artifact_checker.py</code>, <code>npm run check:api-types</code>, <code>npm run typecheck</code>, <code>e2e/session-review.spec.ts</code></td><td>45 backend passed; <code>app.learner_prepost_measure</code> stores learner-scoped aggregate pre/post scores for self-efficacy, skill proficiency, and training satisfaction. <code>scripts/export-phase3-kpi.py</code> emits pseudonymous <code>prepost_measures.csv</code> and KPI report scaffold with paired normalized deltas. Generated API contract exposes <code>UserPrepostMeasure*</code>. 학습자 <code>SessionReview</code>는 1~5 aggregate score 조회/저장 카드를 표시하고, focused E2E 3 passed + visual gate 2 passed.</td></tr>
<tr><td>P1 MVP UI E2E</td><td><code>npx playwright test e2e/session-mvp.spec.ts --project=chromium-single-run</code></td><td>1 passed</td></tr>
<tr><td>P1 MVP UI E2E</td><td><code>npx playwright test e2e/session-mvp.spec.ts --project=chromium-single-run</code></td><td>1 passed</td></tr>
<tr><td>Persona/session review UI E2E</td><td><code>npx playwright test e2e/teacher.spec.ts --project=chromium-single-run</code></td><td>2 passed; teacher approves pending persona and opens pending ended-session review detail from console</td></tr>
<tr><td>Persona/session review UI E2E</td><td><code>npx playwright test e2e/teacher.spec.ts --project=chromium-single-run</code></td><td>2 passed; teacher approves pending persona and opens pending ended-session review detail from console</td></tr>
<tr><td>Voice UI synthetic E2E</td><td><code>npx playwright test e2e/voice-success.spec.ts --project=chromium-single-run</code></td><td>2 passed; direct WS cascade plus Session mic button path with synthetic browser audio and Web Audio playback under blocked media-element autoplay</td></tr>
<tr><td>Voice UI synthetic E2E</td><td><code>npx playwright test e2e/voice-success.spec.ts --project=chromium-single-run</code></td><td>2 passed; direct WS cascade plus Session mic button path with synthetic browser audio and Web Audio playback under blocked media-element autoplay</td></tr>
<tr><td>Learner session archive</td><td><code>python -B -m pytest -p no:cacheprovider app/ -q</code> / <code>python -B -m pytest -p no:cacheprovider engine_gateway/ -q</code> / <code>npm run check:api-types</code> / <code>npm run typecheck</code> / <code>npm run build</code> / <code>npx playwright test e2e/learner.spec.ts --project=chromium-desktop --workers=1</code></td><td>168 backend passed + 9 gateway passed; OpenAPI generated archive DTOs are current; learner history can archive/restore an ended session and keeps archived sessions out of the review queue while preserving records.</td></tr>
<tr><td>Learner session archive</td><td><code>python -B -m pytest app/ -q</code> / <code>python -B -m pytest engine_gateway/ -q</code> / <code>npm run check:api-types</code> / <code>npm run typecheck</code> / <code>npm run build</code> / <code>npx playwright test e2e/learner.spec.ts --project=chromium-desktop --workers=1</code></td><td>178 backend passed + 11 gateway passed; OpenAPI generated archive DTOs are current; learner history can archive/restore an ended session and keeps archived sessions out of the review queue while preserving records.</td></tr>
</tbody>
</tbody>
</table>
</table>
<divclass="tab-subhead"><h4>다음 실행 명령</h4><p>남은 public OAuth <code>/turn</code> proof 확보용 명령이다.</p></div>
<divclass="tab-subhead"><h4>다음 실행 명령</h4><p>남은 public OAuth <code>/turn</code> proof 확보용 명령이다.</p></div>
<divclass="source-note"><b>닫은 항목(live 증거):</b> ① engine config 운영값(<code>claude_cli / 9099 / gateway-default</code>, durable, db). ② 상주 엔진풀 probe — 단일 session_id 2회 reused stream, TTFT 1667–4199ms, cost 누적 0.068→0.123. ③ 상주 엔진풀 RSS — gateway session 1개에서 <code>claude -p</code> child RSS 363.3MB, gateway RSS 16.2MB. ④ Postgres RLS/audit smoke 5 checks PASS. ⑤ turn cost telemetry — <code>app.turns</code> client_ai 13행에 provider/model/cost_usd 실적재(합 $1.22). ⑥ 레이아웃 시각 게이트 7/7 + 적대적 재검수 7/7 accept.</div>
<divclass="source-note"><b>닫은 항목(live 증거):</b> ① engine config 운영값(<code>claude_cli / 9099 / gateway-default</code>, durable, db). ② 상주 엔진풀 probe — 단일 session_id 2회 reused stream, TTFT 1667–4199ms, cost 누적 0.068→0.123. ③ 상주 엔진풀 RSS — gateway session 1개에서 <code>claude -p</code> child RSS 363.3MB, gateway RSS 16.2MB. ④ Postgres RLS/audit smoke 5 checks PASS. ⑤ turn cost telemetry — <code>app.turns</code> client_ai 13행에 provider/model/cost_usd 실적재(합 $1.22). ⑥ 레이아웃 시각 게이트 현재 9/9 + 적대적 재검수 7/7 accept.</div>
<divclass="source-note"><b>환경 제약으로 이 세션에서 못 닫는 항목(정직 표기):</b> 공개 Google OAuth 실제 <code>/turn</code>(실제 계정 storageState 필요) · <code>infra/.env</code> compose 필수값(<code>APP_DB_PASSWORD</code>, <code>OAUTH_GOOGLE_CLIENT_ID</code>, <code>OAUTH_GOOGLE_CLIENT_SECRET</code>) · <code>vnet.18ka.net</code>/<code>api-vnet.18ka.net</code> 공개 런타임(DNS 미해결, Cloudflare 18ka.net zone 권한 불일치, Google OAuth redirect URI 등록 필요) · 한신대 데이터/SSO 거버넌스(owner/기관 written evidence 필요) · 음성 물리 마이크·공개 WSS·50분 양방향 장시간 실측 · claude_cli↔Messages API 폴백 동일성 · 재부팅 후 watchdog smoke · Phase 3 파일럿 게이트 · decisions 6건(owner 결정). 운영 원칙상 가짜 증거로 DONE 표기하지 않는다. 전체 추적: <code>docs/ops/backlog-2026-06-26.md</code>(B1 폴리시 · B2 환경제약 · B3 소유자결정 · B4 거버넌스).</div>
<divclass="source-note"><b>환경 제약으로 이 세션에서 못 닫는 항목(정직 표기):</b> 공개 Google OAuth 실제 <code>/turn</code>(실제 계정 storageState 필요) · <code>infra/.env</code> compose 필수값(<code>APP_DB_PASSWORD</code>, <code>OAUTH_GOOGLE_CLIENT_ID</code>, <code>OAUTH_GOOGLE_CLIENT_SECRET</code>) · <code>vnet.18ka.net</code>/<code>api-vnet.18ka.net</code> 공개 런타임(DNS 미해결, Cloudflare 18ka.net zone 권한 불일치, Google OAuth redirect URI 등록 필요) · 한신대 데이터/SSO 거버넌스(owner/기관 written evidence 필요) · 음성 물리 마이크·공개 WSS·50분 양방향 장시간 실측 · claude_cli↔Messages API 폴백 동일성 · 재부팅 후 watchdog smoke · Phase 3 파일럿 게이트 · decisions 7건(owner 결정). 운영 원칙상 가짜 증거로 DONE 표기하지 않는다. 전체 추적: <code>docs/ops/backlog-2026-06-26.md</code>(B1 폴리시 · B2 환경제약 · B3 소유자결정 · B4 거버넌스).</div>
<divclass="detail-head"><div><h3>윤찬 결정 필요 상세</h3><p>람다가 단독으로 정하면 월권인 항목이다. 결정되기 전에는 "완료"가 아니라 조건부로 둔다.</p></div><divclass="mini-metrics"><span>결정 6개</span><span>조건부 GO 사유</span></div></div>
<divclass="detail-head"><div><h3>윤찬 결정 필요 상세</h3><p>람다가 단독으로 정하면 월권인 항목이다. 결정되기 전에는 "완료"가 아니라 조건부로 둔다.</p></div><divclass="mini-metrics"><span>결정 7개</span><span>조건부 GO 사유</span></div></div>
<divclass="task-list">
<divclass="task-list">
<divclass="task-row r3"><div><spanclass="task-status s-risk">DECIDE</span></div><div><b>s2s 2차 PoC 채택 여부</b><p>캐스케이드 1차는 확정. <code>voice-s2s-poc.md</code> 기준표에 따라 비교 리포트가 나온 뒤 유지/폐기를 정한다. 선택지: 캐스케이드만 / s2s 2차 PoC 유지.</p></div><div><b>영향</b><p>음성 아키텍처, 전송 비용, 로그 보존 방식. 람다 단독 결정 금지.</p></div></div>
<divclass="task-row r3"><div><spanclass="task-status s-risk">DECIDE</span></div><div><b>s2s 2차 PoC 채택 여부</b><p>캐스케이드 1차는 확정. <code>voice-s2s-poc.md</code> 기준표에 따라 비교 리포트가 나온 뒤 유지/폐기를 정한다. 선택지: 캐스케이드만 / s2s 2차 PoC 유지.</p></div><div><b>영향</b><p>음성 아키텍처, 전송 비용, 로그 보존 방식. 람다 단독 결정 금지.</p></div></div>
<divclass="task-row r3"><div><spanclass="task-status s-risk">DECIDE</span></div><div><b>서비스용 페르소나 음성 모델·권리 정책</b><p>Higgs v3 무참조 synthetic seed 방식으로 P1 샘플은 만들었고, 개발 플래그로 P1 sample TTS provider를 서비스에 연결했다. 하지만 실서비스는 모델 상업 라이선스와 reference voice 권리정책이 닫히기 전까지 탑재 금지다. 선택지: Higgs 라이선스 확보 / 동의 받은 ref만 사용 / 대체 TTS provider.</p></div><div><b>영향</b><p>법적 리스크, 음성 품질, 페르소나 확장 비용, 운영 GPU 요구량.</p></div></div>
<divclass="task-row r3"><div><spanclass="task-status s-risk">DECIDE</span></div><div><b>서비스용 페르소나 음성 모델·권리 정책</b><p>Higgs v3 무참조 synthetic seed 방식으로 P1 샘플은 만들었고, 개발 플래그로 P1 sample TTS provider를 서비스에 연결했다. 하지만 실서비스는 모델 상업 라이선스와 reference voice 권리정책이 닫히기 전까지 탑재 금지다. 선택지: Higgs 라이선스 확보 / 동의 받은 ref만 사용 / 대체 TTS provider.</p></div><div><b>영향</b><p>법적 리스크, 음성 품질, 페르소나 확장 비용, 운영 GPU 요구량.</p></div></div>
<divclass="task-row r3"><div><spanclass="task-status s-risk">DECIDE</span></div><div><b>fast-loop 외부 API 허용 여부</b><p>로컬 상주 분류기를 기본으로 하되, 외부 Haiku/Solar API 폴백을 허용할지 정한다. 선택지: 로컬 전용 / 외부 폴백 허용.</p></div><div><b>영향</b><p>데이터주권, 레이턴시, 장애 대응</p></div></div>
<divclass="task-row r3"><div><spanclass="task-status s-risk">DECIDE</span></div><div><b>fast-loop 외부 API 허용 여부</b><p>로컬 상주 분류기를 기본으로 하되, 외부 Haiku/Solar API 폴백을 허용할지 정한다. 선택지: 로컬 전용 / 외부 폴백 허용.</p></div><div><b>영향</b><p>데이터주권, 레이턴시, 장애 대응</p></div></div>
<divclass="task-row r3"><div><spanclass="task-status s-risk">DECIDE</span></div><div><b>운영 보존기간·에스컬레이션 정책</b><p>헬스 raw/rollup 보존 기간, SLA 표현 여부, 운영 티켓 담당 그룹 자동 배정, 우선순위 escalation 조건을 정해야 한다. 현재 구현은 명시 인자 기반 dry-run/apply 경로까지만 제공한다.</p></div><div><b>영향</b><p>운영 감사, 장애 대응, 개인정보·로그 보존 정책</p></div></div>
<divclass="task-row r3"><div><spanclass="task-status s-risk">DECIDE</span></div><div><b>재귀학습 fine-tuning 범위</b><p>1차는 few-shot 자동갱신만 할지, fine-tuning까지 포함할지 결정한다. 선택지: few-shot only / fine-tuning 포함.</p></div><div><b>영향</b><p>동의서, 데이터셋 품질, 운영 리스크</p></div></div>
<divclass="task-row r3"><div><spanclass="task-status s-risk">DECIDE</span></div><div><b>재귀학습 fine-tuning 범위</b><p>1차는 few-shot 자동갱신만 할지, fine-tuning까지 포함할지 결정한다. 선택지: few-shot only / fine-tuning 포함.</p></div><div><b>영향</b><p>동의서, 데이터셋 품질, 운영 리스크</p></div></div>
<divclass="task-row r3"><div><spanclass="task-status s-risk">DECIDE</span></div><div><b>자유연습 기본값</b><p>수정안은 기본 ON + 피드백 권장 배지 + 교수자 토글을 제안한다. 선택지: 기본 ON / 교수자 승인 후 ON.</p></div><div><b>영향</b><p>학습자 자율성, 평가 품질, 안전 정책</p></div></div>
<divclass="task-row r3"><div><spanclass="task-status s-risk">DECIDE</span></div><div><b>자유연습 기본값</b><p>수정안은 기본 ON + 피드백 권장 배지 + 교수자 토글을 제안한다. 선택지: 기본 ON / 교수자 승인 후 ON.</p></div><div><b>영향</b><p>학습자 자율성, 평가 품질, 안전 정책</p></div></div>
<divclass="task-row r3"><div><spanclass="task-status s-risk">DECIDE</span></div><div><b>추가 축어록 수급 규모/일정</b><p>n=1 시드의 한계를 해소하려면 추가 사례 수급 규모와 일정을 정해야 한다. 선택지: N건 목표/일정 확정.</p></div><div><b>영향</b><p>평가 타당도, 페르소나 다양성, Phase 3 측정력</p></div></div>
<divclass="task-row r3"><div><spanclass="task-status s-risk">DECIDE</span></div><div><b>추가 축어록 수급 규모/일정</b><p>n=1 시드의 한계를 해소하려면 추가 사례 수급 규모와 일정을 정해야 한다. 선택지: N건 목표/일정 확정.</p></div><div><b>영향</b><p>평가 타당도, 페르소나 다양성, Phase 3 측정력</p></div></div>
이번 세션은 seed persona 결합 제거, 비개발 fail-closed 설정, compose OAuth 주입, admin 모바일 overflow 수정, persona 인증 경계 보강, 전체 Playwright 재통과, 공개 API prod 전환까지 완료했다. 이번 이어받기에서는 내담자 AI 생성 턴의 provider/model/token/cost 텔레메트리를 <code>/turn</code>, SSE stream, 음성 생성 경로에서 DB <code>app.turns</code>까지 적재하도록 연결했고, gateway stream <code>done/error</code> 계약을 구조화했으며, P1 상담 시작→텍스트 stream→종료→리뷰 피드백 UI E2E와 저항 엔진 정량 회귀 테스트를 추가했다. 이어서 PII 마스킹 게이트 우회 차단, RBAC/IDOR·감사 insert 경로, 음성 paralinguistic 메타 저장, 공개 런타임 watchdog 스크립트와 runbook을 보강했다. 추가로 상주 engine session reuse, SAML provider 스캐폴드, <code>visible_to</code> 런타임 필터, EOT/TTS payload 계약까지 회귀화했다. 백엔드 회귀 테스트는 84개로 재검증했고, 페르소나 승인/반려 API와 교수 콘솔 검수 UI proof를 추가했다. Postgres RLS/audit live smoke checker와 s2s 2차 PoC 판단 메모도 준비했지만, live DB 실행과 s2s 채택 결정은 별도 증거가 필요하다. 이후 로컬 5175 런타임 재기동, admin 사용자 관리 E2E 안정화, 학습자 홈 0회 기록 UX, active session 실제 SSE 스트림 전환, 텍스트/음성 실패 턴 미저장, 직접 URL persona 우회 차단, Live2D demo 자산 제거, AI 운영 fail-closed 보강까지 전체 Playwright로 확인했다. 2026-06-26 15:18 KST 기준 공개 API 터널을 복구한 뒤 전체 Playwright 72개가 desktop/mobile/single-run 모두 통과했다. Cloudflare Pages production 배포 <code>2a58ca49</code>도 완료했고, 커스텀 도메인이 새 asset을 서빙하는 것을 확인했다. 과거 Mao/Haru Live2D demo 자산은 repo와 번들에서 제거했고, <code>/live2d/*</code> 운영 경로는 404로 차단했다. 현재 런타임은 prod API 8001, dev API 8000/8010, web 5173, engine gateway 9099, cloudflared tunnel 1개를 의도적으로 유지한다. 이번 라운드에서는 레이아웃 작업을 서브에이전트 전담으로 분리해 세션, 학습자 홈/리뷰, 관리자/교수자, 설정 화면을 overflow 수리 수준이 아니라 실제 작업 화면 재설계 수준으로 갱신했다. 통합 레이아웃 E2E는 desktop/mobile 54개를 최종 통과했고, 자세한 인수인계는 <code>docs/ops/layout-redesign-handoff-2026-06-26.md</code>에 남겼다. 이번 이어받기에서는 핸드오프가 남긴 "실기기 시각 수용"을 엄격한 자동 게이트로 닫았다. 7개 권장 너비(390/720/861/900/1024/1280/1440)에서 7개 재설계 화면의 가로 overflow 0·컨트롤 클립 0을 강제하는 <code>layout-visual-gate.spec.ts</code>(7 passed, 49장 스크린샷)를 만들고, 화면별 1에이전트 병렬 시각 리뷰 → 6개 병렬 수정 에이전트 → 화면별 적대적 재검수까지 돌려 7/7 accept(critical/major 0, 회귀 0)를 확정했다. 학습자홈 1280 3열 가운데 컬럼 붕괴(critical)와 교수 'API 404' raw 배너를 포함한 결함을 모두 해소했고, 레이아웃 항목을 DONE으로 닫았다. 이번 로그인 복구 라운드에서는 dev/Tailnet OAuth direct start까지 <code>local_oauth_unavailable</code>로 막고, public OAuth state를 HttpOnly 쿠키에 묶인 서명 토큰으로 바꿔 API 재시작 후 콜백이 <code>invalid_state</code>로 실패하지 않게 했으며, callback 실패 reason/status 진단 로그와 화면 메시지 분기를 추가했다. 추가로 Google provider error를 <code>access_denied</code>/<code>provider_error</code>로 분리하고 로그인 화면에 reason code를 노출했다. 이어서 C1 사례개념화 워크시트는 리뷰 API·편집 UI·<code>app.case_worksheet</code> 저장/재조회 경로까지 2차로 연결했다. 공개 Google OAuth 실제 <code>/turn</code> 증명만 storageState 캡처 후 남아 있다.
이번 세션은 seed persona 결합 제거, 비개발 fail-closed 설정, compose OAuth 주입, admin 모바일 overflow 수정, persona 인증 경계 보강, 전체 Playwright 재통과, 공개 API prod 전환까지 완료했다. 이번 이어받기에서는 내담자 AI 생성 턴의 provider/model/token/cost 텔레메트리를 <code>/turn</code>, SSE stream, 음성 생성 경로에서 DB <code>app.turns</code>까지 적재하도록 연결했고, gateway stream <code>done/error</code> 계약을 구조화했으며, P1 상담 시작→텍스트 stream→종료→리뷰 피드백 UI E2E와 저항 엔진 정량 회귀 테스트를 추가했다. 이어서 PII 마스킹 게이트 우회 차단, RBAC/IDOR·감사 insert 경로, 음성 paralinguistic 메타 저장, 공개 런타임 watchdog 스크립트와 runbook을 보강했다. 추가로 상주 engine session reuse, SAML provider 스캐폴드, <code>visible_to</code> 런타임 필터, EOT/TTS payload 계약까지 회귀화했다. 백엔드 회귀 테스트는 84개로 재검증했고, 페르소나 승인/반려 API와 교수 콘솔 검수 UI proof를 추가했다. Postgres RLS/audit live smoke checker와 s2s 2차 PoC 판단 메모도 준비했지만, live DB 실행과 s2s 채택 결정은 별도 증거가 필요하다. 이후 로컬 5175 런타임 재기동, admin 사용자 관리 E2E 안정화, 학습자 홈 0회 기록 UX, active session 실제 SSE 스트림 전환, 텍스트/음성 실패 턴 미저장, 직접 URL persona 우회 차단, Live2D demo 자산 제거, AI 운영 fail-closed 보강까지 전체 Playwright로 확인했다. 2026-06-26 15:18 KST 기준 공개 API 터널을 복구한 뒤 전체 Playwright 72개가 desktop/mobile/single-run 모두 통과했다. Cloudflare Pages production 배포 <code>2a58ca49</code>도 완료했고, 커스텀 도메인이 새 asset을 서빙하는 것을 확인했다. 과거 Mao/Haru Live2D demo 자산은 repo와 번들에서 제거했고, <code>/live2d/*</code> 운영 경로는 404로 차단했다. 현재 런타임은 prod API 8001, dev API 8000/8010, web 5173, engine gateway 9099, cloudflared tunnel 1개를 의도적으로 유지한다. 이번 라운드에서는 레이아웃 작업을 서브에이전트 전담으로 분리해 세션, 학습자 홈/리뷰, 관리자/교수자, 설정 화면을 overflow 수리 수준이 아니라 실제 작업 화면 재설계 수준으로 갱신했다. 통합 레이아웃 E2E는 desktop/mobile 54개를 최종 통과했고, 자세한 인수인계는 <code>docs/ops/layout-redesign-handoff-2026-06-26.md</code>에 남겼다. 이번 이어받기에서는 핸드오프가 남긴 "실기기 시각 수용"을 엄격한 자동 게이트로 닫았다. 7개 권장 너비(390/720/861/900/1024/1280/1440)에서 초기 7개 재설계 화면의 가로 overflow 0·컨트롤 클립 0을 강제하는 <code>layout-visual-gate.spec.ts</code>(초기 7 passed/49장, 현재 빈 회기리뷰 포함 9 passed/63장)를 만들고, 화면별 1에이전트 병렬 시각 리뷰 → 6개 병렬 수정 에이전트 → 화면별 적대적 재검수까지 돌려 7/7 accept(critical/major 0, 회귀 0)를 확정했다. 학습자홈 1280 3열 가운데 컬럼 붕괴(critical)와 교수 'API 404' raw 배너를 포함한 결함을 모두 해소했고, 레이아웃 항목을 DONE으로 닫았다. 이번 로그인 복구 라운드에서는 dev/Tailnet OAuth direct start까지 <code>local_oauth_unavailable</code>로 막고, public OAuth state를 HttpOnly 쿠키에 묶인 서명 토큰으로 바꿔 API 재시작 후 콜백이 <code>invalid_state</code>로 실패하지 않게 했으며, callback 실패 reason/status 진단 로그와 화면 메시지 분기를 추가했다. 추가로 Google provider error를 <code>access_denied</code>/<code>provider_error</code>로 분리하고 로그인 화면에 reason code를 노출했다. 이어서 C1 사례개념화 워크시트는 리뷰 API·편집 UI·<code>app.case_worksheet</code> 저장/재조회 경로까지 2차로 연결했다. 공개 Google OAuth 실제 <code>/turn</code> 증명만 storageState 캡처 후 남아 있다.
1. `init_pool()` → DB 풀 생성, `ensure_runtime_tables()` / `ensure_review_tables()` 보장.
1. `init_pool()` → DB 풀 생성, `ensure_runtime_tables()` / `ensure_review_tables()` 보장.
2. `settings.auto_seed_personas` 면 `materialize_seed_personas()`로 시스템 페르소나 P1~P3과 저장소 `data/personas/P4~P7.json`을 `app.persona_card`에 누락분만 물리화한다. 기존 DB 저작본/보관본은 덮어쓰거나 되살리지 않는다.
2. `settings.auto_seed_personas` 면 `materialize_seed_personas()`로 시스템 페르소나 P1~P3과 저장소 `data/personas/P4~P7.json`을 `app.persona_card`에 누락분만 물리화한다. 기존 DB 저작본/보관본은 덮어쓰거나 되살리지 않는다. 운영자는 `scripts/materialize-persona-seeds.py` dry-run으로 같은 seed/version manifest를 확인하고, 명시적 `--apply`에서만 DB pool을 초기화해 이 materializer를 호출할 수 있다.
- `materialize_seed_personas()` — built-in P1~P7을 초기 승인 카탈로그로 누락분만 insert(admin 롤). `ON CONFLICT DO NOTHING`이므로 교수 편집본을 덮어쓰거나 `archived` 보관본을 재승인하지 않는다.
- `materialize_seed_personas()` — built-in P1~P7을 초기 승인 카탈로그로 누락분만 insert(admin 롤). `ON CONFLICT DO NOTHING`이므로 교수 편집본을 덮어쓰거나 `archived` 보관본을 재승인하지 않는다.
- `scripts/materialize-persona-seeds.py` — API 서버 없이 seed/version manifest를 보고하는 운영 runner. 기본은 dry-run이고, `--json`은 P1~P7 `seed_version`/deterministic `persona_id`/`source_provenance`를 출력하며, `--apply`일 때만 DB pool lifecycle을 감싼 뒤 기존 idempotent DB materializer를 실행한다.
- `create_persona_revision_from_existing()` — 공개 승인본을 같은 `persona_id`의 다음 `version` draft/review로 복제한다. 이미 열린 draft/review가 있으면 새 버전을 만들지 않고 기존 초안을 돌려준다.
- `create_persona_revision_from_existing()` — 공개 승인본을 같은 `persona_id`의 다음 `version` draft/review로 복제한다. 이미 열린 draft/review가 있으면 새 버전을 만들지 않고 기존 초안을 돌려준다.
- `archive_persona_family()` — 교수/관리자 UI의 삭제 동작. 세션 FK 보존을 위해 hard delete 대신 같은 `code` family의 비보관 버전을 모두 `status='archived'`로 전환한다.
- `archive_persona_family()` — 교수/관리자 UI의 삭제 동작. 세션 FK 보존을 위해 hard delete 대신 같은 `code` family의 비보관 버전을 모두 `status='archived'`로 전환한다.
- `get_approved_persona(code)` / `list_approved_personas()` — 승인된 최신 버전 조회(AI 컨텍스트).
- `get_approved_persona(code)` / `list_approved_personas()` — 승인된 최신 버전 조회(AI 컨텍스트).
@ -396,9 +442,17 @@ DB `app.persona_card`가 승인 페르소나의 SoR. 이 모듈이 in-proc `Pers
- 페르소나 스튜디오: `/teach/personas`가 teacher/admin 전용 저작 작업면이다. 교수 콘솔은 진입점/triage만 맡고,
- 페르소나 스튜디오: `/teach/personas`가 teacher/admin 전용 저작 작업면이다. 교수 콘솔은 진입점/triage만 맡고,
실제 저작은 개요·임상·저항·회기·말투·안전·프롬프트 탭으로 분리한다.
실제 저작은 개요·임상·저항·회기·말투·안전·프롬프트 탭으로 분리한다.
- RAG 첨부 SSOT: `POST /personas/sources`는 첨부/붙여넣기 자료를 `mask_pii()` 후
- RAG 첨부 SSOT: `POST /personas/sources`는 첨부/붙여넣기 자료를 `mask_pii()` 후
raw 원문 hash-only 증거는 `kb.raw_source_artifact`에 따로 기록하고, sanitized 파생본만
`kb.source/document/chunk`에 evaluator 전용 근거(`visible_to=['evaluator']`, `sensitivity=2`)로 등록한다.
`kb.source/document/chunk`에 evaluator 전용 근거(`visible_to=['evaluator']`, `sensitivity=2`)로 등록한다.
`rag.index_document()`는 `sensitivity=3` 또는 raw source marker chunk를 DB 접근 전에 거부하므로
raw 원문은 `kb.chunk`/embedding/FTS 검색면에 들어가지 않는다.
`POST /personas/drafts/generate`는 raw text가 아니라 `source_id` 기반 RAG 검색 결과를 생성 근거로 사용하고,
`POST /personas/drafts/generate`는 raw text가 아니라 `source_id` 기반 RAG 검색 결과를 생성 근거로 사용하고,
이 호출은 `(code, version)` 누락분만 insert하며, 교수자가 수정한 DB 저작본이나 `archived` 보관본을 덮어쓰거나 재노출하지 않는다.
이 호출은 `(code, version)` 누락분만 insert하며, 교수자가 수정한 DB 저작본이나 `archived` 보관본을 덮어쓰거나 재노출하지 않는다.
(또는 `apps/api/.env`에 두 키를 `true`로 적어도 된다.)
(또는 `apps/api/.env`에 두 키를 `true`로 적어도 된다.)
seed manifest만 확인할 때는 API 서버 없이 저장소 루트에서 dry-run runner를 실행한다. Windows에서 `python` 별칭이 다른 인터프리터를 가리키면 API 의존성이 없을 수 있으므로, 이 저장소에서는 Python 3.11 런처를 우선 쓴다. `--apply`는 DB pool을 초기화한 뒤 기존 idempotent DB materializer를 호출하므로 로컬 DB 대상이 맞는지 확인한 뒤에만 쓴다.
| **C1** | 사례개념화·치료계획 산출물 저장형 구조 2차 구현·채점/검수 잔여 | `SessionReviewResponse.caseWorksheet`가 축어록 근거 기반 초안을 제공하고, 리뷰 화면에서 학습자가 편집한 워크시트를 `PUT /sessions/{id}/review/worksheet`로 `app.case_worksheet`에 저장한다. 이후 `GET /review`는 `saved_by_learner` 저장본을 자동 초안보다 우선 반환한다. CCD는 숨은 정답키로 남고 저장본에 점수로 노출하지 않는다. | AI 축어록 초안 추출 고도화, 규칙 기반 채점, 교수자 검수 플로우. 루브릭은 임상팀 외부 정의 가능하게 외부화. | doc1·doc4·doc5 |
| **C1** | 사례개념화·치료계획 산출물 저장형 구조 2차 구현·채점/검수 잔여 | `SessionReviewResponse.caseWorksheet`가 축어록 근거 기반 초안을 제공하고, 리뷰 화면에서 학습자가 편집한 워크시트를 `PUT /sessions/{id}/review/worksheet`로 `app.case_worksheet`에 저장한다. 이후 `GET /review`는 `saved_by_learner` 저장본을 자동 초안보다 우선 반환한다. CCD는 숨은 정답키로 남고 저장본에 점수로 노출하지 않는다. | AI 축어록 초안 추출 고도화, 규칙 기반 채점, 교수자 검수 플로우. 루브릭은 임상팀 외부 정의 가능하게 외부화. | doc1·doc4·doc5 |
| **C2** | 위기개입 프로토콜·생명유지서약·에스컬레이션 1차 배선 완료·임상 고도화 필요 | 실제 자해·자살 신호는 LLM/엔진 호출 전 중단하고 109 리소스와 `conversation_stopped`를 REST/SSE/voice 응답에 싣는다. `crisis.risk_level`은 상태머신 `ideation_observed`로 전달하고, escalate 시 `app.safety_events` detail 적재 및 교수자 대시보드 안전 알림 큐로 연결한다. | 남은 것은 임상팀 콘텐츠: 비밀보장 예외고지→단계적 탐색→생명유지서약 스크립트, 실시간 push/메일 알림 정책, 위기탐색 누락 시 회기리뷰 감점 루브릭. | doc1·doc5(핵심 시나리오)·doc4(IRB 전제) |
| **C2** | 위기개입 프로토콜·생명유지서약·에스컬레이션 1차 배선 완료·임상 고도화 필요 | 실제 자해·자살 신호는 LLM/엔진 호출 전 중단하고 109 리소스와 `conversation_stopped`를 REST/SSE/voice 응답에 싣는다. `crisis.risk_level`은 상태머신 `ideation_observed`로 전달하고, escalate 시 `app.safety_events` detail 적재 및 교수자 대시보드 안전 알림 큐로 연결한다. | 남은 것은 임상팀 콘텐츠: 비밀보장 예외고지→단계적 탐색→생명유지서약 스크립트, 실시간 push/메일 알림 정책, 위기탐색 누락 시 회기리뷰 감점 루브릭. | doc1·doc5(핵심 시나리오)·doc4(IRB 전제) |
| **C3** | 이론모드 1차 배선 완료·CBT 콘텐츠/선택 UI 잔여 | `TurnContext`/`prepare_turn`/sessions/voice/evaluator에 `theory_mode`가 전달되고, `build_turn_messages`도 인간중심·CBT·통합 이론 프레이밍을 엔진 메시지에 넣는다. 프론트는 `persona.theory_target` 기준으로 시작해 기존 `humanistic` 하드코딩을 제거했다. | 임상팀이 확정한 CBT 단계 프롬프트 체인, 이론부합 채점 루브릭, 학습자/교수자용 명시적 이론 선택 UI. 현재는 페르소나 설계 이론 자동 선택이다. | doc4(humanistic+CBT 필수)·doc2/doc5 |
| **C3** | 이론모드 2차 배선·학습자 명시 선택 UI 완료·CBT 콘텐츠 잔여 | `TurnContext`/`prepare_turn`/sessions/voice/evaluator에 `theory_mode`가 전달되고, `build_turn_messages`도 인간중심·CBT·통합 이론 프레이밍을 엔진 메시지에 넣는다. 프론트는 `persona.theory_target` 기준 기본값을 잡되, 세션 시작 전 `humanistic`/`cbt`/`integrative` segmented control로 학습자가 명시 선택하고 `POST /sessions`의 `theory_mode`로 전송한다. | 임상팀이 확정한 CBT 단계 프롬프트 체인, 이론부합 채점 루브릭. 이번 UI는 임상 문안·루브릭·백엔드 enum을 확장하지 않았다. | doc4(humanistic+CBT 필수)·doc2/doc5 |
### High (4)
### High (4)
| ID | 갭 | 현재상태 | 권고 | 근거 |
| ID | 갭 | 현재상태 | 권고 | 근거 |
|---|---|---|---|---|
|---|---|---|---|---|
| **H1** | 계약 평가 KPI(자기효능감·기술숙련도·수련만족도 사전사후) 수집·집계 전무 | `자기효능감/사전사후/수련만족/실험통제군` grep 0건. Phase3 KPI도 report shape만, 계산 코드 0줄. (분석) | 3척도 pre-post 폼·실험/통제군 배정·자동누적 대시보드·추이 시각화·검정 계산 코드. (κ/ICC·환각률은 doc4 미명시 → 평가설계 확정.) | doc4(20명 실험/통제군·단회기 50분·3척도 pre-post) |
| **H1** | 계약 평가 KPI(자기효능감·기술숙련도·수련만족도 사전사후) 수집·입력·CSV/report 계산 1차 | `app.learner_prepost_measure`, 학습자 본인용 `GET/PUT /users/me/prepost-measures`, `SessionReview`의 파일럿 증거 원장 카드가 3척도 pre/post 1~5 aggregate evidence를 저장·조회한다. `app.services.phase3_kpi_export`와 `scripts/export-phase3-kpi.py`는 원장 row를 Phase 3 evidence root의 `02-measures/prepost_measures.csv`와 `02-measures/kpi_report.json` scaffold로 산출한다. participant id는 가명화하고, 3척도 paired normalized mean pre/post/delta, complete/missing pair를 계산한다. | 공식 문항 확정, 실험/통제군 배정, 추이 시각화, 통계검정 종류/alpha/결측 처리, 실제 20명 evidence와 steward/legal/IAA 검수. 현재 API/UI/export는 공식 효과성·성적·수료 판정이 아니라 파일럿 evidence 계산이다. (κ/ICC·환각률은 doc4 미명시 → 평가설계 확정.) | doc4(20명 실험/통제군·단회기 50분·3척도 pre-post) |
| **H2** | 턴별 fast-loop + 라이브 코칭 1차 가동·골든셋/리뷰 2열 UI 잔여 | `make_eval_hook`이 submit/voice 생성 경로에 주입되고, stream은 `_evaluate_stream_turn`으로 fast-loop 평가를 붙인다. 결과는 `feedback_scores`, `alternative_utterance` 등 정규화 테이블에 적재·hydrate된다. 추가로 `app/services/live_coach.py`, `POST/GET /sessions/{id}/live-coach`, `POST /kb/live-coach/source-packs/sync`, `app.live_coach_events`, `data/kb/live_coaching_workbook_0615.json`, `data/kb/live_coaching_sources/*.json`을 연결해 워크북·DSM·공식 지침 요약 기반 코칭 아바타 말풍선·근거 모달·발화별 이력 오버레이와 RAG 증분 색인을 제공한다. | 회기리뷰 UI 좌(축어록 타임라인+비언어)/우(기법·적절성·대안반응·이론) 2열 고도화, 원천 축어록 few-shot 골든셋 적재, 임상팀 확정 루브릭과 source pack 버전·citation 운영정책 보강. | doc2·doc5(골드 포맷) |
| **H2** | 턴별 fast-loop + 라이브 코칭 1차 가동·학습자 리뷰 2열 UI 1차 완료·골든셋 잔여 | `make_eval_hook`이 submit/voice 생성 경로에 주입되고, stream은 `_evaluate_stream_turn`으로 fast-loop 평가를 붙인다. 결과는 `feedback_scores`, `alternative_utterance` 등 정규화 테이블에 적재·hydrate된다. 추가로 `app/services/live_coach.py`, `POST/GET /sessions/{id}/live-coach`, `POST /kb/live-coach/source-packs/sync`, `app.live_coach_events`, `data/kb/live_coaching_workbook_0615.json`, `data/kb/live_coaching_sources/*.json`을 연결해 워크북·DSM·공식 지침 요약 기반 코칭 아바타 말풍선·근거 모달·발화별 이력 오버레이와 RAG 증분 색인을 제공한다. `app.services.source_pack_sync`가 repo source pack의 active `content_hash`를 비교하고 변경 시 document version을 최신+1로 올린다. 학습자 `SessionReview` 데스크톱은 좌측 축어록 타임라인, 우측 요약·감정·흐름·루브릭·강점·개선점·pre/post·워크시트·피드백 작업열의 2열 구조로 재배치했다. | 원천 축어록 few-shot 골든셋 적재, 임상팀 확정 루브릭과 source pack 임상 검수 상태 운영정책 보강. | doc2·doc5(골드 포맷) |
| **H3** | 임상팀 콘텐츠 입력 경로(페르소나 저작 CRUD) 2차 구현·임상 검수 잔여 | draft 생성·조회·편집·검수요청 API와 교수 콘솔 JSON 초안 패널은 연결됐다. `persona_repository.py`는 in-code `SEED_PERSONAS`(P1~P3)와 `data/personas/P4.json`~`P7.json`을 `PersonaCard`로 합쳐 `materialize_seed_personas()`와 seed fallback catalog에 포함한다. | JSON 대신 항목형 저작 UI, 루브릭·이론 콘텐츠 외부화, P4~P7 포함 임상팀 최종 검수/서면 evidence 확보. | doc3(R&R)·doc4(페르소나=전문가 산출물) |
| **H3** | 임상팀 콘텐츠 입력 경로(페르소나 저작 CRUD) 2차 구현·원문 격리 정책 1차 완료·임상 검수 잔여 | draft 생성·조회·편집·검수요청 API와 교수 콘솔 JSON 초안 패널은 연결됐다. `persona_repository.py`는 in-code `SEED_PERSONAS`(P1~P3)와 `data/personas/P4.json`~`P7.json`을 `PersonaCard`로 합쳐 `materialize_seed_personas()`와 seed fallback catalog에 포함한다.`scripts/materialize-persona-seeds.py`는 같은 seed manifest를 dry-run 기본으로 보고하고, `--apply`에서만 DB pool을 초기화한 뒤 기존 idempotent DB materializer를 호출한다. `scripts/sync-persona-sources.py`는 DB-backed dry-run/apply runner로 repo-managed source pack의 `content_hash`/document version을 비교한다. RAG 기반 draft 생성은 source/chunk evidence와 함께 `persona-draft-rag@2026-06-28.1` prompt bundle id/version/hash를 engine metadata 및 draft `source_provenance`에 남긴다. `POST /personas/sources`는 raw 원문 hash-only 증거를 `kb.raw_source_artifact`에 따로 기록하고, sanitized 파생본만 evaluator-only RAG chunk로 색인한다. `rag.index_document()`는 `sensitivity=3` 또는 raw marker chunk를 DB 접근 전에 차단한다. | JSON 대신 항목형 저작 UI, 루브릭·이론 콘텐츠 외부화, P4~P7 포함 임상팀 최종 검수/서면 evidence 확보, 암호화 blob/vault 기반 원문 실저장. | doc3(R&R)·doc4(페르소나=전문가 산출물) |
| **H4** | PII 마스킹 한국어 이름/기관 NER + 온보딩·동의 게이트 잔여 | Presidio `language='en'` 고정이라 이름/기관명은 NER 보강이 필요하지만, 한국어 날짜·금액·행정구역 주소 정규식 폴백은 추가됐다. 외부 LLM 호출은 상담 생성(generate/stream)·fast/deep 평가 직후 `audit.llm_call_log`에 provider/model/token/cost/inference_geo/latency만 적재하도록 연결했고, prompt/completion 본문은 저장하지 않는다. 로컬 dev-login 실제 `/turn` smoke에서 `audit.llm_call_log` 3행 증가를 확인했다. `app_user`에 이름·소속·학과·학년/직위·연락처·주소/수령지·닉네임·자기소개·아바타 URL·약관/개인정보 동의 버전 필드를 추가했고, 로그인 직후 `/onboarding` 완료 전에는 역할 홈과 learner 회기 시작을 막는다. 아바타 이미지는 `/users/me/avatar`에서 MIME/시그니처/3MB 제한 후 파일 저장소에 두고 URL만 보관한다. 온보딩 저장 시 learner `consent_at`도 함께 세팅하며 auth E2E에서 신규 계정 온보딩→아바타 업로드→학습자 홈 이동을 검증했다. | 한국어 이름/기관 NER 추가, 미성년/guardian 및 법무 검토가 필요한 최종 서명 동의서·개인정보 처리방침·약관 evidence 확보. 공개 Google OAuth 실제 `/turn` proof는 별도 운영 게이트. | doc1/2/5(실명·날짜·미성년·자살시도 다수)·doc4(IRB·개인정보) |
| **H4** | PII 마스킹 한국어 이름/기관 로컬 1차 + fixture 평가 harness + 온보딩·동의 게이트 잔여 | Presidio `language='en'` 고정이라 한국어 이름/기관 정밀 NER 한계는 남아 있지만, 정규식 폴백에 한국어 날짜·금액·행정구역 주소와 함께 이름/성명 라벨, 성씨+이름+조사/호칭, 대학교·학과·병원·센터 등 기관 suffix 기반 로컬 휴리스틱 마스킹을 추가했다. Presidio가 설치돼도 한국어 누락을 막기 위해 fallback을 후단에 한 번 더 태운다. 상담 생성(generate/stream), fast evaluator prompt, client turn `text_masked`에서 한국어 NAME/ORG raw 값이 남지 않도록 회귀화했다. `app.services.pii_masking_eval`, `data/privacy/pii-masking-ko-fixtures.json`, `scripts/evaluate-pii-masking.py`로 합성 NAME/ORG 5케이스를 entity recall·forbidden substring removal·unexpected entity violation으로 평가하고, `소속`/`안내` NAME 오탐을 stopword로 보정했다. 외부 LLM 호출은 상담 생성(generate/stream)·fast/deep 평가 직후 `audit.llm_call_log`에 provider/model/token/cost/inference_geo/latency만 적재하도록 연결했고, prompt/completion 본문은 저장하지 않는다. 로컬 dev-login 실제 `/turn` smoke에서 `audit.llm_call_log` 3행 증가를 확인했다. `app_user`에 이름·소속·학과·학년/직위·연락처·주소/수령지·닉네임·자기소개·아바타 URL·약관/개인정보 동의 버전 필드를 추가했고, 로그인 직후 `/onboarding` 완료 전에는 역할 홈과 learner 회기 시작을 막는다. 아바타 이미지는 `/users/me/avatar`에서 MIME/시그니처/3MB 제한 후 파일 저장소에 두고 URL만 보관한다. 온보딩 저장 시 learner `consent_at`도 함께 세팅하며 auth E2E에서 신규 계정 온보딩→아바타 업로드→학습자 홈 이동을 검증했다. | 모델 기반 ko NER 정밀화, 실제 운영 말뭉치 기반 오탐/미탐 평가, 미성년/guardian 및 법무 검토가 필요한 최종 서명 동의서·개인정보 처리방침·약관 evidence 확보. 공개 Google OAuth 실제 `/turn` proof는 별도 운영 게이트. | doc1/2/5(실명·날짜·미성년·자살시도 다수)·doc4(IRB·개인정보) |
### Medium+ (6) — M1~M3, X1~X2, L1
### Medium+ (6) — M1~M3, X1~X2, L1
| ID | 갭 | 현재상태 | 권고 |
| ID | 갭 | 현재상태 | 권고 |
|---|---|---|---|
|---|---|---|---|
| **M1** | 비언어/준언어 임상 이벤트 캡처·태깅 부재 | `voice.py`는 EOT용 `silence_ms`만, 침묵·한숨·울음 타임스탬프 이벤트 캡처·리뷰 표시 전무. 서버 RMS 힌트 프론트 미사용(dead). (분석) | 침묵·한숨·울음을 타임스탬프 메타 이벤트로 보존·시각화, '침묵 견디기'를 역량 지표화, 페르소나 의도적 침묵·비유창 한국어 렌더링. |
| **M1** | 비언어/준언어 임상 이벤트 캡처·태깅 4차 진행 | 1차에서 `audio_ref`/`silence_ms`/`speech_rate`/`barge_in`을 learner voice turn에 저장하고 리뷰 `nonverbal` 칩으로 파생했다. 2차에서는 `app.turns.provider_events JSONB`와 `TurnRecord.provider_events`를 추가해 WebSocket control/STT provider 이벤트를 allowlist·size limit 후 보존한다. 3차에서는 저장 전 sanitizer에서 내부 taxonomy `event_type`/`category`를 붙인다. 4차에서는 인증된 회기 리뷰 API가 `sigh`/`cry`/`laugh`/`breath`, prosody, background noise 계열만 한글 label/detail 칩으로 파생 노출한다. raw transcript/text payload, provider/source/raw type, 공개 공유 카드 노출은 제외한다. | 실제 provider 기반 한숨·울음·억양 감지 연결, 장시간 마이크/WSS 실측, 리뷰 칩을 역량 지표로 해석할지에 대한 정책. |
| **M2** | 다회기 종단 케이스 아크·교차회기 사례개념화 부분 구동 | 1차로`(persona_id, learner_id)` 안정 `case_profile` upsert, 원자적 `session_no`, 직전 `session_summary` 기반 seed recall, voice/REST/SSE recall cache 주입을 연결했다. `case_digest`/`pinned_fact` 실적재, episodic embedding writer, trajectory 갱신은 아직 없다. | case_state 런타임 보강, pinned_fact·case_digest 압축/갱신, 접수면접→다회기 연속성·자기개념 진화 실증. |
| **M2** | 다회기 종단 케이스 아크·교차회기 사례개념화 6차 구동 |`(persona_id, learner_id)` 안정 `case_profile` upsert, 원자적 `session_no`, voice/REST/SSE recall cache 주입을 연결했다. 세션 종료 시 마스킹 축어록 기반 fallback `session_summary.digest`, `case_profile.case_digest`, `rapport_trajectory`, `alliance_level`을 갱신하고, 다음 회기 seed recall은 `case_digest`·직전 `session_summary`·client-visible non-contradicted `pinned_fact`를 함께 조립한다. 3차에서는 마스킹된 client-visible 발화에서 `[NAME]`/`[ORG]` identity와 명시적 상담 약속만 보수적으로 `pinned_fact`에 upsert한다. 4차에서는 pinned fact 삽입 또는 값 변경 시 `pinned_fact_history`에 append-only 이력을 남긴다. 5차에서는 명시적 상담 약속 철회/부정만 기존 non-locked `agreement:counseling` fact를 `contradicted`로 격리하고 history reason `contradiction`을 남긴다. 6차에서는 세션 종료 저장 성공 뒤 마스킹된 client-visible 내담자 발화만 `app.turn_embedding`에 BGE-M3 dense/sparse로 idempotent 색인한다. 같은 값 재확인은 history를 늘리지 않고, `locked` fact는 건드리지 않는다. 관계갈등·위기·임상 추론은 자동 pinning/모순 처리에서 제외한다. | 관계·임상 fact 승격 기준, LLM digest 압축, 접수면접→다회기 연속성·자기개념 진화 실증. |
| **M3** | SSO claim 매핑·식별자 안정성 1차 완료·운영 IdP 감사 미연결 | Google/SAML/dev-login이 `AUTH_EMAIL_COHORT_MAP`·`AUTH_DOMAIN_COHORT_MAP` 및 SAML cohort claim을 `cohort_ids`로 전달하고, `app_user.external_id`는 provider subject(`google:`/`saml:`/`dev:`) 기반으로 저장한다. 운영 SAML 서명검증, 기관 claim schema/test tenant, deprovisioning audit은 아직 없다. | 한신 IdP 확정 후 SAML 서명검증, claim→role/cohort/institution_user_id 매핑 표 실연동, role변경/삭제 audit, deprovisioning evidence. |
| **M3** | SSO claim 매핑·식별자 안정성 1차 완료·운영 IdP 감사 미연결 | Google/SAML/dev-login이 `AUTH_EMAIL_COHORT_MAP`·`AUTH_DOMAIN_COHORT_MAP` 및 SAML cohort claim을 `cohort_ids`로 전달하고, `app_user.external_id`는 provider subject(`google:`/`saml:`/`dev:`) 기반으로 저장한다. 운영 SAML 서명검증, 기관 claim schema/test tenant, deprovisioning audit은 아직 없다. | 한신 IdP 확정 후 SAML 서명검증, claim→role/cohort/institution_user_id 매핑 표 실연동, role변경/삭제 audit, deprovisioning evidence. |
| **X1** | 재귀학습·데이터셋 export 파이프라인 1차 구현 | `scripts/export-recursive-dataset.py`와 `app.services.dataset_export`로 masked-text JSONL dry-run, PII scan, kappa/ICC 계산, approved export 게이트를 구현했다. 기본은 `technical_dry_run`이며 실제 승인 export·골든셋 승격은 데이터 steward/legal review와 IAA 통과가 필요하다. | 파일럿 evidence에서 reviewer disposition, steward/legal 승인, gold annotation 라운드 적재 후 `approved_for_recursive_learning_seed` 승격 검증. |
| **X1** | 재귀학습·데이터셋 export 파이프라인 1차 구현 | `scripts/export-recursive-dataset.py`와 `app.services.dataset_export`로 masked-text JSONL dry-run, PII scan, kappa/ICC 계산, approved export 게이트를 구현했다. 기본은 `technical_dry_run`이며 실제 승인 export·골든셋 승격은 데이터 steward/legal review와 IAA 통과가 필요하다. | 파일럿 evidence에서 reviewer disposition, steward/legal 승인, gold annotation 라운드 적재 후 `approved_for_recursive_learning_seed` 승격 검증. |
| **X2** | AI API 비용 관측·예산 경고 1차 완료·평가 저비용 라우팅 1차 완료 | 턴별 provider/model/tokens/cost 저장 경로와 `GET /admin/usage`, 관리자 비용 대시보드를 연결했다. `ADMIN_USAGE_BUDGET_USD` 기준 예산 상태(ok/warn/exceeded)도 응답/UI에 표시한다. `EVALUATOR_FAST_MODEL`/`EVALUATOR_DEEP_MODEL` 설정 시 fast/deep 평가 호출만 해당 모델 override로 gateway에 전달하고, 비워두면 기존 gateway default 라우팅을 유지한다. DB 미가용 dev는 runtime store fallback, prod는 fail-closed다. 캐싱은 L0~L2 cache hint와 gateway session reuse까지만 연결돼 있고 semantic cache·장기 한도 정책은 아직 없다. | semantic cache, 장기 비용 추이/한도 정책, 운영 모델별 비용 검증. |
| **X2** | AI API 비용 관측·예산 경고·평가 저비용 라우팅·evaluator cache 관측·일별 비용 추이·모델별 비용 검증 리포트 2차 완료 | 턴별 provider/model/tokens/cost 저장 경로와 `GET /admin/usage`, 관리자 비용 대시보드를 연결했다. `ADMIN_USAGE_BUDGET_USD` 기준 예산 상태(ok/warn/exceeded)도 응답/UI에 표시한다. `EVALUATOR_FAST_MODEL`/`EVALUATOR_DEEP_MODEL` 설정 시 fast/deep 평가 호출만 해당 모델 override로 gateway에 전달하고, 비워두면 기존 gateway default 라우팅을 유지한다. fast/deep evaluator structured 결과는 canonical request SHA-256 기반 인메모리 semantic cache로 재사용하며, 원문 prompt·completion은 저장하지 않고 성공 파싱 결과만 TTL/entry 제한 안에서 캐시한다. `/admin/usage`와 관리자 비용 카드가 cache enabled/entries/hits/misses/stores/evictions/requests/hit_rate와 일별 `daily_cost` 추이를 노출한다. `app.services.usage_report`와 `scripts/report-ai-usage.py`는 같은 usage JSON에서 provider/model별 cost share, token share, cost/turn, cost/1k tokens, metered coverage, budget/cache warning을 산출한다. DB 미가용 dev는 runtime store fallback, prod는 fail-closed다. | 자동 차단·한도 enforcement 정책. |
| **L1** | 기술스택 신청서-구현 불일치 및 단기일정 산출물 압박 | doc4 신청서 스택(Spring Boot 3/Node.js·TimescaleDB) vs 실제 FastAPI/Python 불일치, 20주 단기일정·9월 저작권 등재 압박. | 스택 정합 또는 변경 사유를 거버넌스 회의록으로, 9월 저작권 등재 문서화 수준을 일정 반영. |
| **L1** | 기술스택 신청서-구현 불일치 및 단기일정 산출물 압박 | doc4 신청서 스택(Spring Boot 3/Node.js·TimescaleDB) vs 실제 FastAPI/Python 불일치, 20주 단기일정·9월 저작권 등재 압박. 소유자 결정으로 장기 교체 대상은 Node.js 우선, 현재 FastAPI 전면 재작성은 보류했다. 내부 전환 증거로 engine gateway 공유 계약, `EngineClient.stream_packets()` decode 경계, schema-backed golden fixture(`engine_gateway_contract.v1.json`/`engine_gateway_schema.v1.json`), Python import 없는 `scripts/check-engine-gateway-contract.mjs` Node.js conformance runner, 브라우저-facing 세션 read-model 분리(`app/session_read_model.py`)까지 고정했다. | 신청서/저작권 등재 문서에 FastAPI 유지 사유와 계약 우선 Node 전환 계획을 반영하는 외부 거버넌스 증거. 다음 내부 후보는 persona DTO/mapper 분리. |
> X2 근거: doc3 회의록이 'AI API 비용'을 운영 리스크로 명시.
> X2 근거: doc3 회의록이 'AI API 비용'을 운영 리스크로 명시.
@ -87,17 +87,17 @@
- **C1 2차 완료**: `caseWorksheet` 응답 구조, 리뷰 화면 편집 UI, `app.case_worksheet` 저장/재조회 경로. 후속은 임상 루브릭·AI 추출/채점·교수자 검수.
- **C1 2차 완료**: `caseWorksheet` 응답 구조, 리뷰 화면 편집 UI, `app.case_worksheet` 저장/재조회 경로. 후속은 임상 루브릭·AI 추출/채점·교수자 검수.
- **C2 1차 완료**: 위기 신호는 엔진 전 중단, 109 안내, `safety_events` 적재, 교수자 알림 큐, `ideation_observed` 전달까지 배선했다. 후속은 임상 스크립트·서약 문안·감점 루브릭.
- **C2 1차 완료**: 위기 신호는 엔진 전 중단, 109 안내, `safety_events` 적재, 교수자 알림 큐, `ideation_observed` 전달까지 배선했다. 후속은 임상 스크립트·서약 문안·감점 루브릭.
- **C3 1차 완료**: `theory_mode`가 세션·평가·생성 프롬프트까지 흐른다. 후속은 임상팀 CBT 체인·이론부합 루브릭·명시적 선택 UI.
- **C3 2차 완료**: `theory_mode`가 세션·평가·생성 프롬프트까지 흐르고, 학습자는 세션 시작 전 기존 3개 모드 중 하나를 명시 선택해 `POST /sessions`로 보낸다. 후속은 임상팀 CBT 체인·이론부합 루브릭.
- **H2 1차+라이브 코칭 완료**: `make_eval_hook`과 stream 평가가 턴 파이프라인에 붙고 정규화 테이블로 적재·복원된다. 대안발화도 `app.alternative_utterance`로 정규화한다. 라이브 코칭은 0615 워크북·DSM·공식 지침 요약/RAG 근거로 코칭 아바타 말풍선·근거 모달·발화별 이력 오버레이까지 연결했다. `POST /kb/live-coach/source-packs/sync`로 같은 source pack을 evaluator 전용 RAG에도 증분 색인한다. 후속은 2열 리뷰 UI·골든셋·임상팀 루브릭·source pack 버전 운영.
- **H2 1차+라이브 코칭+리뷰 2열 UI 완료**: `make_eval_hook`과 stream 평가가 턴 파이프라인에 붙고 정규화 테이블로 적재·복원된다. 대안발화도 `app.alternative_utterance`로 정규화한다. 라이브 코칭은 0615 워크북·DSM·공식 지침 요약/RAG 근거로 코칭 아바타 말풍선·근거 모달·발화별 이력 오버레이까지 연결했다. `POST /kb/live-coach/source-packs/sync`는 공용 source pack sync service를 통해 evaluator 전용 RAG에 증분 색인하고, hash 변경 시 document version을 최신+1로 올린다. 학습자 `SessionReview` 데스크톱은 좌측 축어록 타임라인과 우측 작업열의 2열 구조로 재배치했고, 교수자/모바일 레이아웃은 기존 규칙을 유지한다. 후속은 골든셋·임상팀 루브릭·source pack 임상 검수 상태 운영.
- **H3 2차 완료**: 페르소나 저작 CRUD(draft→review)와 P4~P7 저장소 JSON 로드 경로가 붙었다. 후속은 항목형 저작 UI, 루브릭·이론 콘텐츠 외부화, 임상팀 최종 검수 evidence.
- **H3 8차 완료**: 페르소나 저작 CRUD(draft→review), P4~P7 저장소 JSON 로드, RAG source 기반 draft generation, prompt bundle id/version/hash provenance와 seed/version materializer runner가 붙었다. seed runner는 dry-run/JSON manifest를 제공하고, `--apply`에서만 DB pool을 초기화해 기존 materializer를 호출한다. repo-managed source pack runner는 DB-backed dry-run/apply를 제공하며 active `content_hash`가 바뀐 문서만 최신 version+1로 sync한다. 이번 패스에서 `kb.raw_source_artifact` hash-only 레코드와 `rag.index_document()` raw/sensitivity=3 fail-closed guard를 추가해 raw 원문이 `kb.chunk`/embedding/FTS로 들어가지 않게 했다. 후속은 항목형 저작 UI 고도화, 루브릭·이론 콘텐츠 외부화, 임상팀 최종 검수 evidence, 암호화 blob/vault 기반 원문 실저장.
- **H4(부분)·X1·X2**: 마스킹 한국어 정규식 보강, 외부 LLM 호출 metadata-only `audit.llm_call_log` 적재 경로, 학습자 동의 수락/철회/회기 시작 하드게이트 골격은 완료했다. 한국어 이름/기관 NER와 guardian/legal 서명 evidence는 후속이다. dry-run JSONL export·PII scan·IAA 계산 1차도 완료했고 `ds.*` write는 `--write-dataset` 명시 시에만 수행한다. X2 비용 관측·예산 경고와 evaluator fast/deep 모델 override 1차는 완료했고 semantic cache·장기 한도 정책은 후속. M1/M2도 1차 구현 완료, provider 기반 비언어 감지와 case_digest/pinned_fact 실적재는 후속.
- **H4(부분)·X1·X2**: 한국어 날짜/금액/주소 및 이름/기관 로컬 휴리스틱 마스킹, 합성 fixture 평가 harness, 외부 LLM 호출 metadata-only `audit.llm_call_log` 적재 경로, 학습자 동의 수락/철회/회기 시작 하드게이트 골격은 완료했다. 모델 기반 정밀 ko NER, 실제 운영 말뭉치 기반 평가, guardian/legal 서명 evidence는 후속이다. dry-run JSONL export·PII scan·IAA 계산 1차도 완료했고 `ds.*` write는 `--write-dataset` 명시 시에만 수행한다. X2 비용 관측·예산 경고, evaluator fast/deep 모델 override, evaluator semantic cache, 운영 hit-rate 관측, 일별 비용 추이, 모델별 비용 검증 리포트는 완료했고 자동 차단·한도 enforcement 정책은 후속. M1은 provider_events 보존 슬롯, 내부 taxonomy, 인증 리뷰용 제한 파생 칩까지 완료했고, M2는 보수적 identity/agreement pinned_fact 자동 실적재, append-only history, 명시적 상담 약속 철회 contradiction, episodic embedding writer까지 완료했다. L1은 Node.js conformance runner와 session read-model 분리까지 완료했다. 실제 provider 기반 한숨·울음 감지는 후속.
### B. 소유자 결정 / 외부(임상팀·기관) 의존
### B. 소유자 결정 / 외부(임상팀·기관) 의존
- **임상팀(구훈정·어유경) 산출물**: C1 채점 루브릭·항목 확정, C3 CBT 이론 콘텐츠·프롬프트 체인, C2 위기 스크립트·서약 문안, H2 골든셋 코딩·라이브 코칭 루브릭 검수. DSM/공식 지침/논문은 사용 허가 확인 기준으로 KB 확장 가능하며, 각 source pack은 version/citation/임상 검수 상태를 남긴다. → 코드는 구조를 선제 구축하되 임상 문안과 평가기준은 외부 정의로 받는다.
- **임상팀(구훈정·어유경) 산출물**: C1 채점 루브릭·항목 확정, C3 CBT 이론 콘텐츠·프롬프트 체인, C2 위기 스크립트·서약 문안, H2 골든셋 코딩·라이브 코칭 루브릭 검수. DSM/공식 지침/논문은 사용 허가 확인 기준으로 KB 확장 가능하며, 각 source pack은 version/citation/임상 검수 상태를 남긴다. → 코드는 구조를 선제 구축하되 임상 문안과 평가기준은 외부 정의로 받는다.
- **소유자 평가설계 결정**: H1 실험/통제군 배정·3척도 문항·50분 흐름. κ/ICC·환각률 목표는 doc4 미명시 → 평가설계 문서 확정.
- **소유자 평가설계 결정**: H1 실험/통제군 배정·3척도 문항·50분 흐름. κ/ICC·환각률 목표는 doc4 미명시 → 평가설계 문서 확정.
- doc4/doc3 행정 불일치(참여교수 1명 vs 2명, 서식 연도 '2025' 오기, 연구책임자 표기 불일치, 트웬티온스 성명 공란).
- doc4/doc3 행정 불일치(참여교수 1명 vs 2명, 서식 연도 '2025' 오기, 연구책임자 표기 불일치, 트웬티온스 성명 공란).
- '현재상태'는 grep/코드 사실 기반 적대적 비평 인용에서 시작했으나, **C1·C2·C3·H2·H3·M3은 구현 후 재검증 완료** 기준이다. 그 외(H1·H4·M1·M2·X1·X2·L1)는 착수 전 코드 1차 재확인 권고.
- '현재상태'는 grep/코드 사실 기반 적대적 비평 인용에서 시작했으나, **C1·C2·C3·H1·H2·H3·M1 provider_events 보존/taxonomy/인증 리뷰 파생 칩·M2 case memory/pinned_fact/history/명시철회 contradiction/episodic embedding writer·M3, H4 로컬 마스킹/fixture 평가/감사/온보딩 경로, X1 dry-run export, X2 비용 관측·evaluator routing/cache/hit-rate 관측·모델별 비용 리포트, L1 engine gateway contract/golden/schema/Node conformance runner/session read-model 분리는 구현 후 재검증 완료** 기준이다. 다만 H4의 모델 기반 정밀 NER, guardian/legal evidence, 공개 OAuth `/turn` proof, M1 실제 provider 기반 한숨·울음 감지는 여전히 게이트로 남아 있다. L1 외부 거버넌스는 FastAPI 유지 사유와 Node 전환 계획의 제출/등재 증거가 남아 있다.
분류: **B1 비차단 폴리시** · **B2 환경 제약(증거 생산 불가)** · **B3 소유자 결정** · **B4 외부 거버넌스**
분류: **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 사례개념화 산출물은 저장형 워크시트까지 2차 구조를 만들었고, C2 위기개입 프로토콜과 C3 이론모드 스레딩은 1차 구조를 만들었다. 콘텐츠 정의는 임상팀(구훈정·어유경) 소유라 코드는 구조를 선제 구축하되 임상 문안과 평가기준은 외부 정의로 받는다.
> **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 사례개념화 산출물은 저장형 워크시트까지 2차 구조를 만들었고, C2 위기개입 프로토콜은 1차 구조, C3 이론모드는 2차 명시 선택 UI까지 만들었다. 콘텐츠 정의는 임상팀(구훈정·어유경) 소유라 코드는 구조를 선제 구축하되 임상 문안과 평가기준은 외부 정의로 받는다.
---
---
@ -18,16 +18,20 @@
- [x] **SEO/GEO 기본 신호 + 회기 리뷰 URL 공유 카드** — (2026-06-27 처리) `index.html`에 canonical/description/Open Graph/Twitter Card/JSON-LD를 추가하고, `robots.txt`/`sitemap.xml`/`llms.txt`를 게시했다. 종료된 학습자 회기는 `POST /sessions/{id}/share`가 공개 토큰을 생성하고 `GET /share/session/{token}`이 서버 HTML로 요약·썸네일·JSON-LD를 내려 URL unfurl을 지원한다. `app.session_share_link`에는 토큰 해시와 sanitized preview payload만 저장하며 원문 축어록·학습자 식별자는 포함하지 않는다. 공유 페이지는 교수자 전달용이라 `noindex`를 유지한다. **검증: `pytest app/test_session_share.py app/test_session_turn_persistence.py -q` 21 passed, `python -m pytest app/ -q` 178 passed, `python -m pytest engine_gateway/ -q` 11 passed, `npm run check:api-types`, `npm run typecheck`, `npm run build`, `PLAYWRIGHT_PORT=5174 npm run e2e` 113 passed.** 배포 후 실제 카카오톡/Slack/메일 unfurl 1회 확인 권장.
- [x] **SEO/GEO 기본 신호 + 회기 리뷰 URL 공유 카드** — (2026-06-27 처리) `index.html`에 canonical/description/Open Graph/Twitter Card/JSON-LD를 추가하고, `robots.txt`/`sitemap.xml`/`llms.txt`를 게시했다. 종료된 학습자 회기는 `POST /sessions/{id}/share`가 공개 토큰을 생성하고 `GET /share/session/{token}`이 서버 HTML로 요약·썸네일·JSON-LD를 내려 URL unfurl을 지원한다. `app.session_share_link`에는 토큰 해시와 sanitized preview payload만 저장하며 원문 축어록·학습자 식별자는 포함하지 않는다. 공유 페이지는 교수자 전달용이라 `noindex`를 유지한다. **검증: `pytest app/test_session_share.py app/test_session_turn_persistence.py -q` 21 passed, `python -m pytest app/ -q` 178 passed, `python -m pytest engine_gateway/ -q` 11 passed, `npm run check:api-types`, `npm run typecheck`, `npm run build`, `PLAYWRIGHT_PORT=5174 npm run e2e` 113 passed.** 배포 후 실제 카카오톡/Slack/메일 unfurl 1회 확인 권장.
- [x] **학습자·교수자·운영·페르소나 스튜디오 레이아웃 정렬** — (2026-06-28 처리) 작업형 화면에만 `AppShell wide` 폭 정책을 적용하고, 학습자 홈 회기 목록은 큐형 행으로 정리했다. 교수 콘솔은 검토 대기·위기 알림·페르소나 검수·페르소나 저작실 순서가 DOM과 화면 모두 일치하며, 운영 콘솔 사용자/티켓은 조밀한 운영 행으로 낮췄다. 페르소나 스튜디오는 SSOT 첨부/RAG 생성/항목 편집/검증 큐를 한 작업면에서 유지한다. 리포트와 대표 캡처: `docs/ops/layout-research-2026-06-28/dashboard-layout-alignment-report.md`, `docs/ops/layout-research-2026-06-28/final-gate-*.png`. **검증: `npm run typecheck`, `npm run build`, `npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run --workers=1` 7 passed, `npx playwright test e2e/session-layout.spec.ts --project=chromium-desktop --project=chromium-mobile --workers=1` 8 passed, `npx playwright test e2e/learner.spec.ts e2e/teacher.spec.ts e2e/admin.spec.ts --project=chromium-desktop --workers=1` 17 passed.**
- [x] **학습자·교수자·운영·페르소나 스튜디오 레이아웃 정렬** — (2026-06-28 처리) 작업형 화면에만 `AppShell wide` 폭 정책을 적용하고, 학습자 홈 회기 목록은 큐형 행으로 정리했다. 교수 콘솔은 검토 대기·위기 알림·페르소나 검수·페르소나 저작실 순서가 DOM과 화면 모두 일치하며, 운영 콘솔 사용자/티켓은 조밀한 운영 행으로 낮췄다. 페르소나 스튜디오는 SSOT 첨부/RAG 생성/항목 편집/검증 큐를 한 작업면에서 유지한다. 리포트와 대표 캡처: `docs/ops/layout-research-2026-06-28/dashboard-layout-alignment-report.md`, `docs/ops/layout-research-2026-06-28/final-gate-*.png`. **검증: `npm run typecheck`, `npm run build`, `npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run --workers=1` 7 passed, `npx playwright test e2e/session-layout.spec.ts --project=chromium-desktop --project=chromium-mobile --workers=1` 8 passed, `npx playwright test e2e/learner.spec.ts e2e/teacher.spec.ts e2e/admin.spec.ts --project=chromium-desktop --workers=1` 17 passed.**
- [x] **관리자 권한 위임 1차** — (2026-06-28 처리) 역할(role)과 관리자 페이지 진입권(`app_user.admin_access`)을 분리했다. `AUTH_SUPER_ADMIN_EMAILS` 기본값은 `yunchan@twentyoz.kr`, `hoonjungkoo@hs.ac.kr`이며 슈퍼 관리자만 `/admin/users`에서 관리자 권한을 부여·회수한다. 구성 슈퍼 관리자는 학습자·교수자·관리자 라우트 전환이 가능하고, 일반 학생/교수 계정도 `admin_access=true`면 우측 상단 관리자 진입이 보인다. **검증: `python -m pytest app/test_auth_providers.py app/test_admin_ops.py -q` 32 passed, `npm run check:api-types`, `npm run typecheck`.**
- [x] **관리자 권한 위임 1차** — (2026-06-28 처리) 역할(role)과 관리자 페이지 진입권(`app_user.admin_access`)을 분리했다. `AUTH_SUPER_ADMIN_EMAILS` 기본값은 `yunchan@twentyoz.kr`, `hoonjungkoo@hs.ac.kr`이며 슈퍼 관리자만 `/admin/users`에서 관리자 권한을 부여·회수한다. 구성 슈퍼 관리자는 학습자·교수자·관리자 라우트 전환이 가능하고, 일반 학생/교수 계정도 `admin_access=true`면 우측 상단 관리자 진입이 보인다. **검증: `python -m pytest app/test_auth_providers.py app/test_admin_ops.py -q` 32 passed, `npm run check:api-types`, `npm run typecheck`.**
- [x] **회기 아카이브 저장/복원 API** — (2026-06-28 처리) `/learn/history`의 `보관됨`을 실제 학습자별 보기 상태로 연결했다. 종료 회기는 `POST /sessions/{id}/archive`/`restore`로 보관·복원하고, `app.session_archive_state`는 삭제가 아니라 `archived_at`/`updated_at`만 저장한다. `LearnerSessionSummary.archived`, `SessionArchiveResponse`, `LearnerDashboardOverview.archived_sessions`를 OpenAPI에 고정했고, 보관된 회기는 리뷰 대기 행동 큐에서 빠진다. 회기·턴·리뷰·공유 링크·연구/감사 증거는 삭제하지 않는다. **검증: `python -B -m pytest -p no:cacheprovider app/ -q` 168 passed, `python -B -m pytest -p no:cacheprovider engine_gateway/ -q` 9 passed, `npm run check:api-types`, `npm run typecheck`, `npm run build`, `npx playwright test e2e/learner.spec.ts --project=chromium-desktop --workers=1` 6 passed.**
- [x] **회기 아카이브 저장/복원 API** — (2026-06-28 처리) `/learn/history`의 `보관됨`을 실제 학습자별 보기 상태로 연결했다. 종료 회기는 `POST /sessions/{id}/archive`/`restore`로 보관·복원하고, `app.session_archive_state`는 삭제가 아니라 `archived_at`/`updated_at`만 저장한다. `LearnerSessionSummary.archived`, `SessionArchiveResponse`, `LearnerDashboardOverview.archived_sessions`를 OpenAPI에 고정했고, 보관된 회기는 리뷰 대기 행동 큐에서 빠진다. 회기·턴·리뷰·공유 링크·연구/감사 증거는 삭제하지 않는다. **검증: `python -B -m pytest app/ -q` 178 passed, `python -B -m pytest engine_gateway/ -q` 11 passed, `npm run check:api-types`, `npm run typecheck`, `npm run build`, `npx playwright test e2e/learner.spec.ts --project=chromium-desktop --workers=1` 6 passed.**
- [x] **OpenAI TTS voice preset DB map 연결** — (2026-06-28 처리) `app.persona_voice_map`의 OpenAI row를 `/voice/ws` TTS voice 선택에 연결했다. 명시 query preset은 DB map보다 우선하고, 세션 바인딩은 `app.sessions.persona_id/persona_version`으로 voice map을 찾으며, dev persona 생성은 catalog persona id/version map을 사용한다. seed materializer는 기본 OpenAI voice map을 `ON CONFLICT DO NOTHING`으로 생성하고, dev 런타임 스키마 보강은 기존 DB의 `persona_voice_map` 누락도 복구한다. OpenAI가 아닌 provider row는 기존 persona-code fallback으로 안전하게 흡수한다. **검증: `pytest app/test_runtime_policy.py app/test_persona_review.py app/test_voice_service.py app/test_voice_ws.py -q` 75 passed, `pytest app/ -q` 175 passed, `pytest engine_gateway/ -q` 9 passed.**
- [x] **OpenAI TTS voice preset DB map 연결** — (2026-06-28 처리) `app.persona_voice_map`의 OpenAI row를 `/voice/ws` TTS voice 선택에 연결했다. 명시 query preset은 DB map보다 우선하고, 세션 바인딩은 `app.sessions.persona_id/persona_version`으로 voice map을 찾으며, dev persona 생성은 catalog persona id/version map을 사용한다. seed materializer는 기본 OpenAI voice map을 `ON CONFLICT DO NOTHING`으로 생성하고, dev 런타임 스키마 보강은 기존 DB의 `persona_voice_map` 누락도 복구한다. OpenAI가 아닌 provider row는 기존 persona-code fallback으로 안전하게 흡수한다. **검증: `pytest app/test_runtime_policy.py app/test_persona_review.py app/test_voice_service.py app/test_voice_ws.py -q` 75 passed, `pytest app/ -q` 178 passed, `pytest engine_gateway/ -q` 11 passed.**
- [ ] **빈상태 컬럼 높이 여백** — session-review 1280 masonry 우측 컬럼, session-prestart 우측 '진행 초점' 컬럼, learner-home 1440 하단 등 데이터 0건 빈상태에서만 두드러지는 컬럼 높이 불균형/여백. 실데이터면 채워진다. **(미착수, 의도적)** 0건 전용 시각 균형이라 러닝 앱에서의 시각 반복 없이 블라인드 수정 시 악화 위험 → 스택 가용 시 시각 확인하며 처리. 빈상태 전용 균형 레이아웃 채택 자체가 별도 폴리시 결정.
- [x] **빈상태 컬럼 높이 여백** — (2026-06-28 처리) session-review 0건 상태는 1280px 이상에서 3컬럼 masonry를 쓰지 않고 빈 리뷰 전용 2컬럼 순차 레이아웃으로 전환한다. 가짜 기록·장식 콘텐츠를 넣지 않고, `EMPTY_REVIEW_SESSION_ID` fixture와 `session-review-empty` visual gate로 390/720/861/900/1024/1280/1440 전 폭을 검증한다. session-prestart와 learner-home은 기존 gate 대상에 남겨 함께 무회귀 확인했다. **검증: `npm run typecheck`, `npm run build`, `npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run --workers=1` 9 passed, `npx playwright test e2e/session-layout.spec.ts e2e/session-review.spec.ts --project=chromium-desktop --workers=1` 6 passed.**
- [x] **운영 티켓·헬스 샘플 이력 저장 모델 1차** — `/admin` 운영 홈과 `/admin/tickets`가 `app.admin_health_event`·`app.support_ticket` 실제 DB 모델에 연결됐다. `/users/support-tickets`는 인증 사용자의 문제·불만·장애 접수를 저장하고, `/admin/tickets`는 미해결 큐·우선순위·상태 변경을 처리한다. `/admin/uptime`은 상시 SLA가 아니라 관리자 콘솔이 관측한 헬스 샘플 정상률/중단 이력을 반환한다. 가짜 티켓 수·가짜 다운타임 숫자는 계속 금지. **검증: `python -m pytest app/test_admin_ops.py -q` 3 passed, `npm run generate:api-types`, `npm run typecheck`.**
- [x] **운영 티켓·헬스 샘플 이력 저장 모델 1차** — `/admin` 운영 홈과 `/admin/tickets`가 `app.admin_health_event`·`app.support_ticket` 실제 DB 모델에 연결됐다. `/users/support-tickets`는 인증 사용자의 문제·불만·장애 접수를 저장하고, `/admin/tickets`는 미해결 큐·우선순위·상태 변경을 처리한다. `/admin/uptime`은 상시 SLA가 아니라 관리자 콘솔이 관측한 헬스 샘플 정상률/중단 이력을 반환한다. 가짜 티켓 수·가짜 다운타임 숫자는 계속 금지. **검증: `python -m pytest app/test_admin_ops.py -q` 3 passed, `npm run generate:api-types`, `npm run typecheck`.**
- [ ] **운영 티켓·가용성 고도화 정책** — 1차 DB 큐는 연결됐지만 보존 기간, 담당 그룹 자동 배정, 우선순위 escalation, 사용자별 티켓 조회 UI, 상시 synthetic monitor/cron 샘플러, 헬스 이벤트 retention·rollup은 아직 운영 정책 결정과 후속 구현이 필요하다.
- [x] **사용자별 운영 티켓 조회 UI** — (2026-06-28 처리) 학습자 Settings에 `지원 요청` 섹션을 추가해 본인이 접수한 티켓의 상태·카테고리·우선순위·담당 그룹·해결 메모를 읽기 전용으로 확인한다. `GET /users/support-tickets`는 `reporter_id = current user` 기준으로만 조회하고, 본문과 신고자 이메일/이름은 응답·감사 검증에서 제외한다. **검증: `python -B -m pytest app/test_user_support_tickets.py app/test_admin_ops.py app/test_runtime_policy.py -q` 29 passed, `npm run generate:api-types`, `npm run check:api-types`, `npm run typecheck`, `npm run build`, `npx playwright test e2e/settings.spec.ts --project=chromium-desktop --workers=1` 7 passed, `npx playwright test e2e/admin.spec.ts --project=chromium-desktop --workers=1 --grep "operation tickets"` 1 passed.**
- [x] **상시 synthetic monitor/cron 샘플러 1차** — (2026-06-28 처리) `/admin/health`와 같은 헬스 계산을 `record_admin_health_sample()`로 공통화하고, 브라우저 세션 없이 `scripts/record-admin-health-sample.py`가 `app.admin_health_event`에 서비스별 샘플을 append한다. Windows 예약 실행은 `scripts/install-health-sampler-task.ps1`가 `VignetteAdminHealthSampler` Scheduled Task로 설치한다. 이 값은 SLA가 아니라 synthetic sampler가 관측한 최근 샘플 이력이다. **검증: `python -B -m py_compile apps/api/app/routes/admin.py apps/api/app/test_admin_ops.py scripts/record-admin-health-sample.py`, `python scripts/record-admin-health-sample.py --help`, `powershell -NoProfile -ExecutionPolicy Bypass -File scripts\install-health-sampler-task.ps1 -PrintOnly -IntervalMinutes 7`, `python -B -m pytest app/test_admin_ops.py app/test_runtime_policy.py app/test_user_support_tickets.py -q` 31 passed, 로컬 one-shot `recorded_count=5`, `status=ok`, `engine_mode=claude_cli`.**
- [x] **헬스 이벤트 retention/rollup 실행 경로 1차** — (2026-06-28 처리) raw `app.admin_health_event`와 일별 `app.admin_health_daily_rollup`을 분리했다. `scripts/maintain-admin-health-events.py`는 `--rollup-days`와 `--retention-days`를 명시해야 하며 기본은 dry-run이다. `--apply`는 rollup을 먼저 upsert한 뒤 retention window 밖 raw 샘플만 삭제하고, non-dev apply는 `--allow-non-dev-apply` 없이는 차단한다. `/admin/uptime` 요약은 raw 이벤트와 daily rollup을 중복 없이 합산하고, 상세 `events` 목록은 최근 raw 샘플만 유지한다. **검증: `python -B -m py_compile apps/api/app/db.py apps/api/app/auth_sessions.py apps/api/app/routes/admin.py apps/api/app/services/admin_health_maintenance.py scripts/maintain-admin-health-events.py`, `python scripts/maintain-admin-health-events.py --help`, `python -B -m pytest app/test_admin_health_maintenance.py app/test_admin_ops.py app/test_runtime_policy.py app/test_user_support_tickets.py -q` 36 passed, 로컬 dry-run `rollup_event_count=0`, `prunable_event_count=0`.**
- [ ] **운영 티켓·가용성 정책 결정** — 1차 DB 큐, 사용자별 조회 UI, synthetic sampler, retention/rollup 실행 경로는 연결됐지만 raw/rollup 보존 기간의 운영 기본값, 담당 그룹 자동 배정, 우선순위 escalation은 아직 운영 정책 결정이 필요하다. `/admin/uptime`과 sampler 값은 계속 SLA가 아니라 관측 샘플 이력이다.
- [x] **운영 티켓 필터·카테고리 큐·수동 처리 감사 1차** — (2026-06-28 처리) `/admin/tickets`가 status/category/priority/assigned_group/source_path/stale/search 필터를 서버 query로 처리하고, 운영 콘솔은 검색·상태·카테고리·우선순위·담당 그룹·정체 필터와 카테고리 quick queue를 제공한다. 티켓 접수와 관리자 수동 상태/우선순위/담당그룹/해결메모 변경은 `audit.audit_log`에 metadata-only(`support_ticket_create`, `support_ticket_update`)로 남기며, 제목/본문 전문은 감사 로그에 복제하지 않는다. **검증: `pytest app/test_admin_ops.py app/test_runtime_policy.py -q` 28 passed, `npm run generate:api-types`, `npm run check:api-types`, `npm run typecheck`.**
- [x] **운영 티켓 필터·카테고리 큐·수동 처리 감사 1차** — (2026-06-28 처리) `/admin/tickets`가 status/category/priority/assigned_group/source_path/stale/search 필터를 서버 query로 처리하고, 운영 콘솔은 검색·상태·카테고리·우선순위·담당 그룹·정체 필터와 카테고리 quick queue를 제공한다. 티켓 접수와 관리자 수동 상태/우선순위/담당그룹/해결메모 변경은 `audit.audit_log`에 metadata-only(`support_ticket_create`, `support_ticket_update`)로 남기며, 제목/본문 전문은 감사 로그에 복제하지 않는다. **검증: `pytest app/test_admin_ops.py app/test_runtime_policy.py -q` 28 passed, `npm run generate:api-types`, `npm run check:api-types`, `npm run typecheck`.**
- [ ] **운영 티켓 자동 분류·처리 후속** — 중복 티켓 저장 모델(`fingerprint`, parent/child, merge/unmerge), Claude Recipe headless 자동 수정 후보, 관리자 승인 후 이슈 등록·PR/작업 스레드 생성, 처리 결과 audit trail 확장은 아직 설계/승인 필요. 자동 수정은 운영자 승인 전까지 실행하지 않는다.
- [x] **운영 티켓 중복 저장·수동 연결 1차** — (2026-06-28 처리) `app.support_ticket`에 결정론적 `fingerprint`와 `parent_ticket_id`를 추가했다. 사용자 티켓 생성은 fingerprint를 저장하지만 자동 병합·자동 담당그룹·자동 우선순위 변경은 하지 않는다. `/admin/tickets`는 같은 fingerprint의 중복 후보 수와 oldest parent 후보를 반환하고, 관리자는 기존 `PATCH /admin/tickets/{id}`로 parent 연결/해제를 수동 처리한다. self-parent와 parent cycle은 서버가 거부하며, 변경 감사는 `support_ticket_update` metadata-only로 남기고 subject/body 전문은 복제하지 않는다. Settings의 사용자별 티켓 조회에는 fingerprint/parent를 노출하지 않는다. **검증: `python -B -m py_compile app/auth_sessions.py app/routes/admin.py app/routes/users.py app/services/support_tickets.py`, `python -B -m pytest app/test_admin_health_maintenance.py app/test_admin_ops.py app/test_runtime_policy.py app/test_user_support_tickets.py -q` 38 passed, `npm run generate:api-types`, `npm run check:api-types`, `npm run typecheck`, `npm run build`, `npx playwright test e2e/admin.spec.ts --project=chromium-desktop --workers=1 --grep "operation tickets|duplicate"` 2 passed.**
- [ ] **운영 티켓 자동 분류·처리 후속** — Claude Recipe headless 자동 수정 후보, 관리자 승인 후 이슈 등록·PR/작업 스레드 생성, 자동 담당 그룹 배정, 우선순위 escalation, 처리 결과 audit trail 확장은 아직 설계/승인 필요. 자동 수정은 운영자 승인 전까지 실행하지 않는다.
- [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.**
- [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) 무회귀. 이번 2026-06-28 대시보드 정렬 라운드는 `npm run typecheck`, `npm run build`, layout gate 7/7, session-layout desktop/mobile 8/8, learner+teacher+admin E2E 17/17까지 확인했다.
검증 기준: 변경 후 `npm run typecheck`, `e2e/layout-visual-gate.spec.ts`(현재 9/9), 레이아웃 포커스 E2E(54), `e2e/session-layout.spec.ts`(8/8) 무회귀. 이번 후속 라운드는 빈상태 레이아웃(`typecheck`, `build`, layout gate 9/9, session-layout+session-review desktop 6/6), 사용자별 티켓 조회 UI(backend 29, settings 7, admin ticket 1), synthetic health sampler(backend 31, one-shot recorded_count 5), health retention/rollup(backend 36, dry-run OK), 티켓 중복 저장·수동 연결(backend 38, API types/typecheck/build OK, admin tickets E2E 2)을 확인했다.
- [ ] **자유연습 기본값** — 기본 ON(+피드백 권장 배지+교수자 토글) / 교수자 승인 후 ON. 영향: 학습자 자율성·평가 품질·안전 정책.
- [ ] **자유연습 기본값** — 기본 ON(+피드백 권장 배지+교수자 토글) / 교수자 승인 후 ON. 영향: 학습자 자율성·평가 품질·안전 정책.
- [ ] **추가 축어록 수급 규모·일정** — 목표 N건/일정 확정. 영향: 평가 타당도·페르소나 다양성·Phase 3 측정력.
- [ ] **추가 축어록 수급 규모·일정** — 목표 N건/일정 확정. 영향: 평가 타당도·페르소나 다양성·Phase 3 측정력.
- [x] **백엔드 언어 방향** — (2026-06-28 결정) Node.js를 장기 교체 대상과 신규 분리 서비스 우선 스택으로 삼되, 현재 FastAPI 전면 재작성은 납품 일정 리스크라 보류한다. 전환은 계약 우선 strangler 방식으로 진행한다. 1차 경계로 `app/contracts/engine_gateway.py`를 추가해 FastAPI client와 현재 Python gateway가 `/v1/generate`, `/v1/stream`, SSE `token/done/error` 계약을 공유한다. 결정 기록: `docs/decisions/backend-node-transition.md`. **검증: `python -B -m py_compile app/contracts/engine_gateway.py app/engine_client.py engine_gateway/gateway.py`, `python -B -m pytest -p no:cacheprovider engine_gateway/test_gateway_model.py -q` 11 passed.**
- [x] **백엔드 언어 방향** — (2026-06-28 결정) Node.js를 장기 교체 대상과 신규 분리 서비스 우선 스택으로 삼되, 현재 FastAPI 전면 재작성은 납품 일정 리스크라 보류한다. 전환은 계약 우선 strangler 방식으로 진행한다. 1차 경계로 `app/contracts/engine_gateway.py`를 추가해 FastAPI client와 현재 Python gateway가 `/v1/generate`, `/v1/stream`, SSE `token/done/error` 계약을 공유한다. 이어서 raw gateway SSE line 해석을 `EngineClient.stream_packets()`로 올려 API orchestrator가 `EngineGatewaySsePacket`만 처리하게 했고, Python gateway `/v1/generate` 응답은 `GenerateResponse.model_dump()`로 고정해 hand-mirrored dict drift를 줄였다. 2차로 `apps/api/engine_gateway/golden/engine_gateway_contract.v1.json`과 `engine_gateway_schema.v1.json`을 추가하고, 3차로 `scripts/check-engine-gateway-contract.mjs` Node.js conformance runner를 붙여 Node gateway가 Python import 없이 request/response/SSE packet conformance를 검증할 수 있게 했다. 4차로 `app/session_read_model.py`를 추가해 세션 목록·대시보드·상세·리뷰·공유 payload의 브라우저-facing DTO와 deterministic builder를 `routes/sessions.py`에서 분리했다. route는 auth/RLS DB read/persistence와 session lifecycle을 계속 소유한다. 결정 기록: `docs/decisions/backend-node-transition.md`. **검증: `node scripts/check-engine-gateway-contract.mjs --json`, `python -B -m pytest -p no:cacheprovider engine_gateway/test_gateway_model.py -q` 19 passed, `python -B -m pytest -p no:cacheprovider engine_gateway/test_gateway_model.py app/test_orchestrator_masking.py app/test_session_turn_persistence.py -q` 50 passed, `python -B -m py_compile app/session_read_model.py app/routes/sessions.py`, session read-model focused 48 passed, `npm run check:api-types`, `npm run typecheck`.**
- 평가 SSOP 2차: `TurnRecord.evaluation`을 `app.feedback_scores`/`turn_technique`/`turn_client_state`/`supervisor_comment`/`alternative_utterance`로 정규화 적재·복원하고, submit/stream/voice 저장 경로를 `app.turn_runtime`으로 공통화했다. 원시 평가 테이블은 evaluator/admin RLS로 제한하고 서버가 evaluator 컨텍스트로 적재한다. `app.turns` insert는 `RETURNING id` 확인으로 silent success 제거. **검증: `pytest app/test_evaluation_persistence.py -q` 6 passed, 관련 회귀 45 passed, `pytest app/` 119 passed, `pytest engine_gateway/` 7 passed.**
- 평가 SSOP 2차: `TurnRecord.evaluation`을 `app.feedback_scores`/`turn_technique`/`turn_client_state`/`supervisor_comment`/`alternative_utterance`로 정규화 적재·복원하고, submit/stream/voice 저장 경로를 `app.turn_runtime`으로 공통화했다. 원시 평가 테이블은 evaluator/admin RLS로 제한하고 서버가 evaluator 컨텍스트로 적재한다. `app.turns` insert는 `RETURNING id` 확인으로 silent success 제거. **검증: `pytest app/test_evaluation_persistence.py -q` 6 passed, 관련 회귀 45 passed, `pytest app/` 119 passed, `pytest engine_gateway/` 7 passed.**
- 라이브 코칭 1차 + 이력 UI/RAG sync: `app/services/live_coach.py`, `data/kb/live_coaching_workbook_0615.json`, `data/kb/live_coaching_sources/*.json`, `POST /sessions/{id}/live-coach`, `GET /sessions/{id}/live-coach`, `POST /kb/live-coach/source-packs/sync`, `app.live_coach_events`를 추가해 0615 사례개념화 워크북·DSM·공식 지침 요약 기반 코칭을 구조화했다. 세션 UI는 코칭 모드에서 AI 코치 아바타 말풍선, 근거 모달, 학습자 발화 우측 `C` 마커, 채팅 위 스크롤 이력 오버레이를 제공한다. 저장 payload는 PII 마스킹 excerpt와 코칭 구조화 JSON으로 제한한다. 공식 자료는 허가된 source pack의 version/citation/summary로만 코칭에 넣고 장문 원문·공식 문항 재현은 하지 않는다. 같은 source pack은 관리자 sync로 evaluator 전용 RAG에 증분 색인한다. **검증: `python -m compileall app -q`, `pytest app/test_live_coach_sources.py app/test_orchestrator_masking.py app/test_session_turn_persistence.py -q` 27 passed, `npm run generate:api-types`, `npm run check:api-types`, `npm run typecheck`, `npm run build`, `npm run e2e -- e2e/layout-visual-gate.spec.ts e2e/session-layout.spec.ts` 15 passed. 로컬 DB smoke: sync 4 sources/15 chunks embedded, 재호출 skipped_unchanged 4, `official_suicide_risk_guidelines` source_id 필터 검색 성공.**
- 라이브 코칭 1차 + 이력 UI/RAG sync + 학습자 리뷰 2열 UI: `app/services/live_coach.py`, `data/kb/live_coaching_workbook_0615.json`, `data/kb/live_coaching_sources/*.json`, `POST /sessions/{id}/live-coach`, `GET /sessions/{id}/live-coach`, `POST /kb/live-coach/source-packs/sync`, `app.live_coach_events`를 추가해 0615 사례개념화 워크북·DSM·공식 지침 요약 기반 코칭을 구조화했다. 세션 UI는 코칭 모드에서 AI 코치 아바타 말풍선, 근거 모달, 학습자 발화 우측 `C` 마커, 채팅 위 스크롤 이력 오버레이를 제공한다. 저장 payload는 PII 마스킹 excerpt와 코칭 구조화 JSON으로 제한한다. 공식 자료는 허가된 source pack의 version/citation/summary로만 코칭에 넣고 장문 원문·공식 문항 재현은 하지 않는다. 같은 source pack은 `app.services.source_pack_sync`로 evaluator 전용 RAG에 증분 색인하며, active `content_hash`가 바뀌면 `kb.document.version`을 최신+1로 올려 UNIQUE 충돌을 피한다. 학습자 `SessionReview` 데스크톱은 좌측 축어록 타임라인과 우측 요약·감정·흐름·루브릭·강점·개선점·pre/post·워크시트·피드백 작업열의 2열 구조로 재배치했고, 교수자/모바일 레이아웃은 기존 규칙을 유지한다. **검증: `python -m compileall app -q`, 과거`pytest app/test_live_coach_sources.py app/test_orchestrator_masking.py app/test_session_turn_persistence.py -q` 27 passed, source sync `python -B -m pytest -p no:cacheprovider app/test_live_coach_sources.py -q` 4 passed, `py -3.11 scripts\sync-persona-sources.py --help`,`npm run generate:api-types`, `npm run check:api-types`, `npm run typecheck`, `npm run build`, `npm run e2e -- e2e/layout-visual-gate.spec.ts e2e/session-layout.spec.ts` 15 passed. 2열 UI 검증: `npm run typecheck`, `npx playwright test e2e/session-review.spec.ts --project=chromium-desktop --workers=1` 3 passed, `npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run --workers=1` 9 passed, `session-review__1440-desktop.png` 직접 확인. 로컬 DB smoke: sync 4 sources/15 chunks embedded, 재호출 skipped_unchanged 4, `official_suicide_risk_guidelines` source_id 필터 검색 성공.**
- 계약 SSOT 4차: FastAPI OpenAPI를 `apps/web/src/lib/api.gen.ts`로 생성하는 `npm run generate:api-types`와 stale 체크용 `npm run check:api-types`를 유지한다. `TurnResponse`/`SessionEndResponse`에 이어 `MeResponse`, `AuthConfigResponse`, `ConsentResponse`, `PersonaSummary`, `PersonaReviewSummary`, admin/user/engine DTO, review leaf DTO, `CrisisResourceResponse`, teacher safety/growth leaf DTO, `LearnerSessionsResponse`, `LearnerDashboardResponse`, `SessionReviewResponse`, worksheet aggregate, `TeacherDashboardResponse`, `TeacherSessionSummary`, `TeacherLearnerGrowth`, `SessionStartResponse`, `SessionDetailResponse`, `SessionDetailTurn`를 생성 타입 alias로 전환했다. 세션 응답 stage는 `StageLabel` enum으로 OpenAPI에 고정했고, generated optional/default 차이는 UI form state, notification default helper, 배열 렌더링 fallback으로 흡수한다. `.github/workflows/api-contract.yml`은 PR/master push에서 `npm run check:api-types` drift를 막는다. **검증: `npm run check:api-types`, `npm run typecheck`, `npm run build`, `python -m pytest app/test_session_turn_persistence.py -q` 16 passed, `learner.spec.ts + session-review.spec.ts + teacher.spec.ts + layout-visual-gate.spec.ts` 32 passed.**
- 계약 SSOT 4차: FastAPI OpenAPI를 `apps/web/src/lib/api.gen.ts`로 생성하는 `npm run generate:api-types`와 stale 체크용 `npm run check:api-types`를 유지한다. `TurnResponse`/`SessionEndResponse`에 이어 `MeResponse`, `AuthConfigResponse`, `ConsentResponse`, `PersonaSummary`, `PersonaReviewSummary`, admin/user/engine DTO, review leaf DTO, `CrisisResourceResponse`, teacher safety/growth leaf DTO, `LearnerSessionsResponse`, `LearnerDashboardResponse`, `SessionReviewResponse`, worksheet aggregate, `TeacherDashboardResponse`, `TeacherSessionSummary`, `TeacherLearnerGrowth`, `SessionStartResponse`, `SessionDetailResponse`, `SessionDetailTurn`를 생성 타입 alias로 전환했다. 세션 응답 stage는 `StageLabel` enum으로 OpenAPI에 고정했고, generated optional/default 차이는 UI form state, notification default helper, 배열 렌더링 fallback으로 흡수한다. `.github/workflows/api-contract.yml`은 PR/master push에서 `npm run check:api-types` drift를 막는다. **검증: `npm run check:api-types`, `npm run typecheck`, `npm run build`, `python -m pytest app/test_session_turn_persistence.py -q` 16 passed, `learner.spec.ts + session-review.spec.ts + teacher.spec.ts + layout-visual-gate.spec.ts` 32 passed.**
- 위기게이트 109 1차: 실제 자해·자살 신호는 LLM/엔진 호출 전 중단하고 `crisis_resource(109)`와 `conversation_stopped`를 REST/SSE/voice 응답에 싣는다. `app.safety_events` detail 적재와 교수자 대시보드 안전 알림 큐까지 연결했다. 안전 이벤트 DB insert payload도 회귀화했다. 임상 문안, 실시간 push/메일, 지역 자원 확장은 후속. **검증: `pytest app/test_session_turn_persistence.py app/test_voice_ws.py -q` 20 passed, `pytest app/test_teacher_dashboard.py app/test_session_turn_persistence.py app/test_voice_ws.py -q` 21 passed.**
- 위기게이트 109 1차: 실제 자해·자살 신호는 LLM/엔진 호출 전 중단하고 `crisis_resource(109)`와 `conversation_stopped`를 REST/SSE/voice 응답에 싣는다. `app.safety_events` detail 적재와 교수자 대시보드 안전 알림 큐까지 연결했다. 안전 이벤트 DB insert payload도 회귀화했다. 임상 문안, 실시간 push/메일, 지역 자원 확장은 후속. **검증: `pytest app/test_session_turn_persistence.py app/test_voice_ws.py -q` 20 passed, `pytest app/test_teacher_dashboard.py app/test_session_turn_persistence.py app/test_voice_ws.py -q` 21 passed.**
- 교수자 성장 추적/리뷰 상세 진입 1차: `teacher_dashboard`가 정규화된 턴 평가를 기반으로 학습자별 적절성·라포·기술 사용 추이를 집계하고, 교수 콘솔에 최근 회기/항목별 성장 카드와 안전 알림 큐를 함께 표시한다. 종료 회기 리뷰 대기 행과 최근 회기 행은 `/teach/session/:sessionId/review`로 이어지고, 교수자는 회기 리뷰와 사례개념화 워크시트를 읽기 전용으로 검토한다. 학습자 워크시트 저장은 계속 learner 전용이다. **검증: `pytest app/test_teacher_dashboard.py app/test_session_turn_persistence.py app/test_voice_ws.py -q` 21 passed, `pytest app/test_rbac_idor.py app/test_teacher_dashboard.py -q` 9 passed, `npm run check:api-types`, `npm run typecheck`, `npm run build`, `npx playwright test e2e/teacher.spec.ts --project=chromium-single-run --workers=1` 4 passed.**
- 교수자 성장 추적/리뷰 상세 진입 1차: `teacher_dashboard`가 정규화된 턴 평가를 기반으로 학습자별 적절성·라포·기술 사용 추이를 집계하고, 교수 콘솔에 최근 회기/항목별 성장 카드와 안전 알림 큐를 함께 표시한다. 종료 회기 리뷰 대기 행과 최근 회기 행은 `/teach/session/:sessionId/review`로 이어지고, 교수자는 회기 리뷰와 사례개념화 워크시트를 읽기 전용으로 검토한다. 학습자 워크시트 저장은 계속 learner 전용이다. **검증: `pytest app/test_teacher_dashboard.py app/test_session_turn_persistence.py app/test_voice_ws.py -q` 21 passed, `pytest app/test_rbac_idor.py app/test_teacher_dashboard.py -q` 9 passed, `npm run check:api-types`, `npm run typecheck`, `npm run build`, `npx playwright test e2e/teacher.spec.ts --project=chromium-single-run --workers=1` 4 passed.**
- 교수자 검토 상태 1차: `app.session_review_status`에 교수자 회기 검토 상태를 저장하고, teacher dashboard 응답에 `review_status/review_note/reviewed_at`를 포함한다. `PUT /teacher/sessions/{session_id}/review-status`로 교수자 메모 저장과 검토 완료 처리를 수행하며, 완료된 회기는 pending queue에서 제외한다. 교수자/관리자의 `/sessions/{id}/review` 읽기 허용은 유지하고, 학습자 워크시트 저장은 learner 전용으로 계속 제한한다. 교수 콘솔 검토 큐는 숨김 처리하지 않고 상단 triage로 노출하며, 학습자 리뷰 화면에는 교수자 전용 grid 빈칸을 예약하지 않는다. **검증: `pytest app/test_teacher_dashboard.py app/test_rbac_idor.py app/test_learner_dashboard.py -q` 12 passed, `npm run check:api-types`, `npm run typecheck`, `npm run build`, `teacher.spec.ts` 4 passed, `session-review.spec.ts` 2 passed, `layout-visual-gate.spec.ts` 7 passed.**
- 교수자 검토 상태 1차: `app.session_review_status`에 교수자 회기 검토 상태를 저장하고, teacher dashboard 응답에 `review_status/review_note/reviewed_at`를 포함한다. `PUT /teacher/sessions/{session_id}/review-status`로 교수자 메모 저장과 검토 완료 처리를 수행하며, 완료된 회기는 pending queue에서 제외한다. 교수자/관리자의 `/sessions/{id}/review` 읽기 허용은 유지하고, 학습자 워크시트 저장은 learner 전용으로 계속 제한한다. 교수 콘솔 검토 큐는 숨김 처리하지 않고 상단 triage로 노출하며, 학습자 리뷰 화면에는 교수자 전용 grid 빈칸을 예약하지 않는다. **검증: `pytest app/test_teacher_dashboard.py app/test_rbac_idor.py app/test_learner_dashboard.py -q` 12 passed, `npm run check:api-types`, `npm run typecheck`, `npm run build`, `teacher.spec.ts` 4 passed, `session-review.spec.ts` 2 passed, `layout-visual-gate.spec.ts` 7 passed.**
- 학습자 개인화 대시보드 1차: 교수자 성장 집계와 중복되던 점수/라포/기법 계산을 `app.services.session_metrics`로 공용화하고, `GET /sessions/dashboard`가 본인 세션 기반 `overview/growth/persona_progress/achievements/recent_feedback`를 반환한다. 학습자 홈은 누적 회기, 리뷰 대기, 최근 평가, 라포 흐름, 페르소나별 진행, 최근 피드백, 마지막 세션 리캡을 실제 서버 데이터로 표시한다. 성취는 공식 등급/수료가 아니라 실제 연습 milestone만 사용한다. **검증: `pytest app/test_learner_dashboard.py app/test_teacher_dashboard.py app/test_rbac_idor.py -q` 12 passed, `npm run check:api-types`, `npm run typecheck`, `npm run build`, `learner.spec.ts` 6 passed, `session-review.spec.ts` 2 passed, `teacher.spec.ts` 4 passed, `layout-visual-gate.spec.ts` 7 passed, `session-layout.spec.ts` 4 passed.**
- 학습자 개인화 대시보드 1차: 교수자 성장 집계와 중복되던 점수/라포/기법 계산을 `app.services.session_metrics`로 공용화하고, `GET /sessions/dashboard`가 본인 세션 기반 `overview/growth/persona_progress/achievements/recent_feedback`를 반환한다. 학습자 홈은 누적 회기, 리뷰 대기, 최근 평가, 라포 흐름, 페르소나별 진행, 최근 피드백, 마지막 세션 리캡을 실제 서버 데이터로 표시한다. 성취는 공식 등급/수료가 아니라 실제 연습 milestone만 사용한다. **검증: `pytest app/test_learner_dashboard.py app/test_teacher_dashboard.py app/test_rbac_idor.py -q` 12 passed, `npm run check:api-types`, `npm run typecheck`, `npm run build`, `learner.spec.ts` 6 passed, `session-review.spec.ts` 2 passed, `teacher.spec.ts` 4 passed, `layout-visual-gate.spec.ts` 7 passed, `session-layout.spec.ts` 4 passed.**
- 페르소나 스튜디오/RAG 저작 4차: 교수 콘솔의 JSON 패널을 분리해 teacher/admin 전용 `/teach/personas` 3열 스튜디오(좌: 저작 흐름·카탈로그·검수 큐, 중앙: RAG 자료 등록·항목형 편집 탭, 우: 검증·RAG 근거·검수 결정)로 옮겼다. 2026-06-27 UI 정리에서 중복 네이티브 파일 입력을 커스텀 SSOT 첨부 드롭존으로 교체하고, 저작 스테퍼·탭별 작성 가이드·검증 dot 정렬을 보강했다. 레이아웃 근거와 생성 시안은 `docs/ops/layout-research-2026-06-27/persona-dashboard-layout-guideline.md`에 묶었다. `POST /personas/sources`는 첨부/붙여넣기 자료를 PII 마스킹 후 `kb.source/document/chunk`에 evaluator 전용(`visible_to=['evaluator']`, `sensitivity=2`) 근거 문서로 등록하고,`POST /personas/drafts/generate`는 `source_id` 기반 RAG 검색 결과만 생성 프롬프트에 넘긴다. 생성 응답과 draft `source_provenance`에는 source id, doc/content hash, chunk id를 남긴다. P1~P7 시스템 페르소나는 DB 저작 카탈로그의 초기 부트스트랩으로 승격했고, `materialize_seed_personas()`는 누락분만 insert해서 교수 편집본이나 `archived` 보관본을 덮어쓰지 않는다. 교수자는 공개 목록에서 승인본을 다음 버전 draft로 복제해 수정하고, 불필요한 페르소나는 기존 회기 FK 보존을 위해 같은 code family 전체를 `archived`로 보관 처리한다. 원문 격리본(sensitivity=3) 별도 보관, source/version 전용 마이그레이션 runner, prompt-bundle version pinning은 후속. **검증: `python -m pytest app/test_persona_review.py -q` 31 passed, `npm run generate:api-types`, `npm run check:api-types`, `npm run typecheck`, `npm run build`,`npx playwright test e2e/teacher.spec.ts --project=chromium-single-run --workers=1` 6 passed, `layout-visual-gate.spec.ts` 8 passed, `session-layout.spec.ts` desktop/mobile 8 passed.**
- 페르소나 스튜디오/RAG 저작 8차: 교수 콘솔의 JSON 패널을 분리해 teacher/admin 전용 `/teach/personas` 3열 스튜디오(좌: 저작 흐름·카탈로그·검수 큐, 중앙: RAG 자료 등록·항목형 편집 탭, 우: 검증·RAG 근거·검수 결정)로 옮겼다. 2026-06-27 UI 정리에서 중복 네이티브 파일 입력을 커스텀 SSOT 첨부 드롭존으로 교체하고, 저작 스테퍼·탭별 작성 가이드·검증 dot 정렬을 보강했다. 레이아웃 근거와 생성 시안은 `docs/ops/layout-research-2026-06-27/persona-dashboard-layout-guideline.md`에 묶었다. `POST /personas/sources`는 첨부/붙여넣기 자료를 PII 마스킹 후 raw 원문 hash-only 증거를 `kb.raw_source_artifact`에 따로 기록하고, sanitized 파생본만`kb.source/document/chunk`에 evaluator 전용(`visible_to=['evaluator']`, `sensitivity=2`) 근거 문서로 등록한다. `rag.index_document()`는 `sensitivity=3` 또는 raw marker chunk를 DB 접근 전에 차단해 raw 원문이 `kb.chunk`/embedding/FTS에 들어가지 않게 한다.`POST /personas/drafts/generate`는 `source_id` 기반 RAG 검색 결과만 생성 프롬프트에 넘긴다. 생성 응답과 draft `source_provenance`에는 source id, doc/content hash, chunk id, prompt bundle id/version/hash(`persona-draft-rag@2026-06-28.1`)를 남긴다. P1~P7 시스템 페르소나는 DB 저작 카탈로그의 초기 부트스트랩으로 승격했고, `materialize_seed_personas()`는 누락분만 insert해서 교수 편집본이나 `archived` 보관본을 덮어쓰지 않는다. `scripts/materialize-persona-seeds.py`는 같은 seed/version manifest를 dry-run 기본으로 보고하며, `--apply`일 때만 DB pool을 초기화한 뒤 기존 idempotent materializer를 호출한다. `scripts/sync-persona-sources.py`와 `app.services.source_pack_sync`는 repo-managed source pack의 active `content_hash`를 DB에서 비교하고, 변경 시 `kb.document.version`을 최신+1로 색인한다. 교수자는 공개 목록에서 승인본을 다음 버전 draft로 복제해 수정하고, 불필요한 페르소나는 기존 회기 FK 보존을 위해 같은 code family 전체를 `archived`로 보관 처리한다. 암호화 blob/vault 기반 원문 실저장, 임상팀 최종 검수/서면 evidence, 루브릭·이론 콘텐츠 외부화는 후속. **검증: `python -B -m py_compile app/services/rag.py app/routes/personas.py app/routes/kb.py app/test_persona_review.py app/test_live_coach_sources.py`, `python -B -m pytest -p no:cacheprovider app/test_persona_review.py app/test_live_coach_sources.py -q` 38 passed, `py -3.11 scripts\materialize-persona-seeds.py --json` dry-run `manifest_count=7`, `py -3.11 scripts\sync-persona-sources.py --help`, 기존`npx playwright test e2e/teacher.spec.ts --project=chromium-single-run --workers=1` 6 passed, `layout-visual-gate.spec.ts` 8 passed, `session-layout.spec.ts` desktop/mobile 8 passed.**
- M1 비언어 이벤트 1차: voice learner turn에 이미 저장하던 `audio_ref`/`silence_ms`/`speech_rate`/`barge_in`을 리뷰 API `nonverbal` 이벤트로 노출하고, 회기 리뷰 축어록에 침묵·발화 속도·끼어듦·음성 입력 칩을 표시했다. 한숨·울음 감지와 live 마이크/STT/TTS 실측은 후속. **검증: `pytest app/test_session_turn_persistence.py app/test_voice_ws.py -q` 17 passed, `npm run check:api-types`, `npm run typecheck`, `npm run build`, `session-review.spec.ts` 1 passed.**
- M1 비언어 이벤트 4차: voice learner turn에 이미 저장하던 `audio_ref`/`silence_ms`/`speech_rate`/`barge_in`을 리뷰 API `nonverbal` 이벤트로 노출하고, 회기 리뷰 축어록에 침묵·발화 속도·끼어듦·음성 입력 칩을 표시했다. 2차에서는 `app.turns.provider_events JSONB`와 `TurnRecord.provider_events`를 추가해 WebSocket control/STT provider 이벤트를 allowlist·size limit 후 보존했고, 3차에서는 저장 전 sanitizer에서 내부 taxonomy `event_type`/`category`를 붙인다. 4차에서는 인증된 회기 리뷰에 한숨·울음·웃음·호흡·운율·배경소음 계열만 한글 label/detail 칩으로 파생 노출한다. raw transcript/text/provider/source/raw type은 응답에서 제외하고, 공개 공유 카드에는 축어록과 provider raw를 싣지 않는다. 실제 provider 기반 한숨·울음 감지, 역량 지표화 정책, live 마이크/WSS 장시간 실측은 후속. **검증: `python -B -m py_compile app/routes/sessions.py app/test_session_turn_persistence.py`, `python -B -m pytest -p no:cacheprovider app/test_voice_ws.py app/test_session_turn_persistence.py app/test_voice_service.py -q` 45 passed, `npm run check:api-types`, `npm run typecheck`, `npm run build`, `npx playwright test e2e/session-review.spec.ts --project=chromium-desktop --workers=1` 3 passed.**
- M2 다회기 케이스 아크 1차: DB 세션 생성 시 `(persona_id, learner_id)` 기준 `case_profile`을 upsert하고, `session_no`를 트랜잭션 안에서 원자 증가시키며, `InProcSession.case_id`가 매회 새 `runtime_case_id`가 아니라 안정 `case_id`를 가리키게 수정했다. 시작/submit/stream/voice 경로는 직전 `session_summary` 기반 seed recall을 사용한다. `case_digest`/`pinned_fact` 실적재, episodic embedding writer, trajectory 갱신은 후속. **검증: `pytest app/test_session_turn_persistence.py app/test_voice_ws.py -q` 18 passed, `pytest app/ -q` 119 passed, `npm run check:api-types`.**
- M2 다회기 케이스 아크 6차: DB 세션 생성 시 `(persona_id, learner_id)` 기준 `case_profile`을 upsert하고, `session_no`를 트랜잭션 안에서 원자 증가시키며, `InProcSession.case_id`가 매회 새 `runtime_case_id`가 아니라 안정 `case_id`를 가리키게 수정했다. 시작/submit/stream/voice 경로는 case recall cache를 사용한다. 세션 종료 시 마스킹 축어록 기반 fallback `session_summary.digest`와 `case_profile.case_digest`, `rapport_trajectory`, `alliance_level`을 갱신하고, 다음 회기 seed recall에서 `case_digest`·직전 `session_summary`·client-visible non-contradicted `pinned_fact`를 함께 조립한다. 3차에서는 마스킹된 client-visible 발화에서 `[NAME]`/`[ORG]` identity와 명시적 상담 약속만 보수적으로 `pinned_fact`에 upsert했고, 4차에서는 삽입 또는 값 변경 시 `pinned_fact_history`에 append-only 이력을 남긴다. 5차에서는 명시적 상담 약속 철회/부정만 기존 non-locked `agreement:counseling` fact를 `contradicted`로 격리하고 history reason `contradiction`을 남긴다. 6차에서는 세션 종료 저장 성공 뒤 마스킹된 client-visible 내담자 발화만 `app.turn_embedding`에 BGE-M3 dense/sparse로 `ON CONFLICT (turn_id) DO NOTHING` 색인한다. 새 contradicted fact는 임의 생성하지 않고, 같은 값 재확인은 history를 늘리지 않으며, `locked` fact는 건드리지 않는다. learner-owned `case_profile` 기준 RLS insert/update를 추가했고, 관계갈등·위기·임상 추론은 자동 pinning/모순 처리에서 제외했다. 관계·임상 fact 승격 기준과 LLM digest 압축은 후속. **검증: `python -B -m py_compile app/services/rag.py app/routes/sessions.py app/test_session_memory.py`, `python -B -m pytest -p no:cacheprovider app/test_session_memory.py -q` 14 passed, `python -B -m pytest -p no:cacheprovider app/test_session_memory.py app/test_session_turn_persistence.py app/test_orchestrator_masking.py app/test_runtime_policy.py -q` 67 passed.**
- C1 사례개념화 워크시트 2차: `SessionReviewResponse.caseWorksheet`와 리뷰 화면 카드가 탐색 11항목·호소 5영역·인지삼제/감정·보호/방해·생물심리사회 목표 초안을 축어록 근거 기반으로 제공한다. 학습자가 편집한 저장본은 `PUT /sessions/{id}/review/worksheet`로 `app.case_worksheet`에 저장되고, 이후 `GET /review`는 `saved_by_learner` 저장본을 자동 초안보다 우선 반환한다. 임상 루브릭, AI 추출/채점, 교수자 검수는 후속. **검증: `pytest app/test_session_turn_persistence.py app/test_rbac_idor.py -q` 25 passed, `npm run check:api-types`, `npm run typecheck`, `npm run build`, 로컬 API smoke 세션 생성→저장→`GET /review``saved_by_learner:local smoke saved worksheet`.**
- C1 사례개념화 워크시트 2차: `SessionReviewResponse.caseWorksheet`와 리뷰 화면 카드가 탐색 11항목·호소 5영역·인지삼제/감정·보호/방해·생물심리사회 목표 초안을 축어록 근거 기반으로 제공한다. 학습자가 편집한 저장본은 `PUT /sessions/{id}/review/worksheet`로 `app.case_worksheet`에 저장되고, 이후 `GET /review`는 `saved_by_learner` 저장본을 자동 초안보다 우선 반환한다. 임상 루브릭, AI 추출/채점, 교수자 검수는 후속. **검증: `pytest app/test_session_turn_persistence.py app/test_rbac_idor.py -q` 25 passed, `npm run check:api-types`, `npm run typecheck`, `npm run build`, 로컬 API smoke 세션 생성→저장→`GET /review``saved_by_learner:local smoke saved worksheet`.**
- C3 이론모드 1차: `theory_mode`가 `TurnContext`/sessions/voice/evaluator뿐 아니라 `build_turn_messages`의 엔진 메시지까지 전달된다. 프론트는 `persona.theory_target` 기준으로 시작해 `humanistic` 하드코딩을 제거했다. CBT 체인·이론부합 루브릭·명시적 선택 UI는 후속. **검증: `pytest app/test_orchestrator_masking.py app/test_session_turn_persistence.py -q` 20 passed, `pytest app/ -q` 119 passed.**
- C3 이론모드 2차: `theory_mode`가 `TurnContext`/sessions/voice/evaluator뿐 아니라 `build_turn_messages`의 엔진 메시지까지 전달된다. 프론트는 `persona.theory_target` 기준 기본값을 유지하되, 세션 시작 전 `humanistic`/`cbt`/`integrative` segmented control로 학습자가 명시 선택하고 `POST /sessions`의 `theory_mode`로 보낸다. CBT 체인·이론부합 루브릭은 후속. **검증: `python -B -m pytest -p no:cacheprovider app/test_orchestrator_masking.py app/test_session_turn_persistence.py -q` 31 passed, `npm run typecheck`, `npm run build`, `npx playwright test e2e/session-layout.spec.ts --project=chromium-desktop --workers=1` 4 passed, `npx playwright test e2e/session-layout.spec.ts --project=chromium-mobile --workers=1` 4 passed, `npx playwright test e2e/session-mvp.spec.ts --project=chromium-single-run --workers=1` 1 passed, `npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run --workers=1`9 passed.**
- M3 인증 claim 1차: Google/SAML/dev-login이 `AUTH_EMAIL_COHORT_MAP`·`AUTH_DOMAIN_COHORT_MAP` 및 SAML cohort claim을 `cohort_ids`로 전달하고, DB `app_user.external_id`는 provider subject(`google:`/`saml:`/`dev:`) 기반으로 저장한다. 운영 SAML 서명검증, 기관 claim schema/test tenant, deprovisioning audit은 후속. **검증: `pytest app/test_auth_providers.py -q`22 passed, `pytest app/ -q` 119 passed.**
- M3 인증 claim 1차: Google/SAML/dev-login이 `AUTH_EMAIL_COHORT_MAP`·`AUTH_DOMAIN_COHORT_MAP` 및 SAML cohort claim을 `cohort_ids`로 전달하고, DB `app_user.external_id`는 provider subject(`google:`/`saml:`/`dev:`) 기반으로 저장한다. 운영 SAML 서명검증, 기관 claim schema/test tenant, deprovisioning audit은 후속. **검증: `pytest app/test_auth_providers.py -q`30 passed, `pytest app/ -q` 178 passed.**
- X2 예산 경고/저비용 평가 라우팅 1차: `ADMIN_USAGE_BUDGET_USD` 설정값을 기준으로 `GET /admin/usage`가 `budget.status=disabled|ok|warn|exceeded`, 사용률, 잔여 예산을 반환하고 관리자 콘솔이 예산 상태 배너를 표시한다. 80% 이상 warn, 100% 이상 exceeded. `EVALUATOR_FAST_MODEL`/`EVALUATOR_DEEP_MODEL` 설정 시 fast/deep 평가 호출만 해당 모델 override로 gateway에 전달하고, 비워두면 기존 gateway default 라우팅을 유지한다. semantic cache·장기 한도 정책은 후속. **검증: `pytest app/test_evaluator_model_routing.py app/test_runtime_policy.py -q` 23 passed, `pytest app/ -q` 122 passed, `npm run check:api-types`, `npm run typecheck`.**
- X2 예산 경고/저비용 평가 라우팅/evaluator cache 관측·일별 비용 추이·모델별 비용 검증 리포트 2차: `ADMIN_USAGE_BUDGET_USD` 설정값을 기준으로 `GET /admin/usage`가 `budget.status=disabled|ok|warn|exceeded`, 사용률, 잔여 예산을 반환하고 관리자 콘솔이 예산 상태 배너를 표시한다. 80% 이상 warn, 100% 이상 exceeded. `EVALUATOR_FAST_MODEL`/`EVALUATOR_DEEP_MODEL` 설정 시 fast/deep 평가 호출만 해당 모델 override로 gateway에 전달하고, 비워두면 기존 gateway default 라우팅을 유지한다. fast/deep evaluator structured 결과는 canonical request SHA-256 기반 인메모리 semantic cache로 재사용하며, 원문 prompt·completion은 저장하지 않고 성공 파싱 결과만 TTL/entry 제한 안에서 캐시한다. `/admin/usage`와 관리자 `AI 비용` 카드는 cache enabled/entries/hits/misses/stores/evictions/requests/hit_rate와 일별 `daily_cost` 추이를 표시하되 cache key·prompt·completion은 노출하지 않는다. `app.services.usage_report`와 `scripts/report-ai-usage.py`는 동일 usage JSON에서 provider/model별 cost share, token share, cost/turn, cost/1k tokens, metered coverage, budget/cache warning을 산출한다. 자동 차단·한도 enforcement 정책은 후속. **검증: `python -B -m py_compile app/services/usage_report.py app/test_usage_report.py ..\..\scripts\report-ai-usage.py`, `python -B -m pytest -p no:cacheprovider app/test_usage_report.py app/test_evaluator_model_routing.py app/test_runtime_policy.py app/test_admin_ops.py -q` 42 passed, `scripts/report-ai-usage.py` sample CLI schema `vignette.ai_usage_model_cost_report.v1` 생성, prior `npm run check:api-types`, `npm run typecheck`, `npm run build`, admin E2E evidence remains valid because API response shape was not changed.**
- H4 LLM call audit 1차: 상담 생성(generate/stream)·fast-loop 평가·deep-loop 평가의 외부 LLM 호출 직후 `audit.llm_call_log`에 provider/model/token/cost/inference_geo/latency metadata만 적재한다. prompt/completion 본문은 저장하지 않고 감사 실패는 상담 루프를 막지 않는다. 로컬 dev-login 실제 `/turn` smoke에서 `audit.llm_call_log`가 8→11로 3행 증가했다(session `2460d56c-a9cb-4a40-a175-9575d510a5e9`). 공개 Google OAuth 실제 `/turn` proof는 별도 B2 항목에 남긴다. **검증: `pytest app/test_orchestrator_masking.py app/test_evaluation_persistence.py app/test_session_turn_persistence.py -q` 27 passed,`pytest app/ -q` 119 passed.**
- H4 LLM call audit/마스킹 2차: 상담 생성(generate/stream)·fast-loop 평가·deep-loop 평가의 외부 LLM 호출 직후 `audit.llm_call_log`에 provider/model/token/cost/inference_geo/latency metadata만 적재한다. prompt/completion 본문은 저장하지 않고 감사 실패는 상담 루프를 막지 않는다. 2026-06-28에는 한국어 이름/기관 로컬 휴리스틱 마스킹 1차를 추가했고, fast evaluator prompt의 내담자 응답과 client turn `text_masked`도 마스킹본을 쓰게 보강했다. 이어서 `app.services.pii_masking_eval`, `data/privacy/pii-masking-ko-fixtures.json`, `scripts/evaluate-pii-masking.py`로 합성 NAME/ORG 5케이스 평가 harness를 추가했고, `소속`/`안내` NAME 오탐을 stopword로 보정했다. 로컬 dev-login 실제 `/turn` smoke에서 `audit.llm_call_log`가 8→11로 3행 증가했다(session `2460d56c-a9cb-4a40-a175-9575d510a5e9`). 공개 Google OAuth 실제 `/turn` proof는 별도 B2 항목에 남긴다. **검증: `python -B -m py_compile app/services/guardrail.py app/services/pii_masking_eval.py app/test_pii_masking_eval.py scripts/evaluate-pii-masking.py`, `python -B -m pytest -p no:cacheprovider app/test_pii_masking_eval.py app/test_orchestrator_masking.py app/test_evaluation_persistence.py app/test_session_turn_persistence.py -q` 41 passed, `python -X utf8 scripts/evaluate-pii-masking.py --json` 5/5 pass, 과거 전체 기준`pytest app/ -q` 119 passed.**
- H4 온보딩·동의 게이트 3차: `app_user`에 이름·소속·학과·학년/직위·전화번호·주소/수령지·닉네임·자기소개·아바타 URL·약관/개인정보 동의 시각·버전 필드를 추가했다. 로그인 직후 `/onboarding` 완료 전에는 전역 `OnboardingGate`가 `/onboarding` 외 모든 앱 URL(`/learn`, `/settings`, `/admin`, `/dev/avatar-preview`, `/login` 포함)을 온보딩으로 이동시키고, 온보딩 중에는 공용 셸 메뉴 없이 가입 정보 입력 폼만 보여준다. learner `POST /sessions`와 voice dev persona 시작은 `onboarding_required`를 먼저 확인하고, 온보딩 저장 시 learner `consent_at`도 함께 세팅한다. 아바타는 `/users/me/avatar`에서 PNG/JPG/WebP MIME·시그니처·3MB 제한을 통과한 파일만 `USER_UPLOAD_DIR/profile-avatars`에 저장하고 DB에는 URL만 보관한다. 약관·개인정보 처리방침은 개인정보 보호법·처리방침 작성지침·안전성 확보조치 기준·약관규제법 취지를 반영한 개발 초안으로 `/users/legal-docs`에서 제공한다. 관리자/교수자 권한은 온보딩 화면에서 신청받지 않고 기존 `AUTH_ADMIN_EMAILS`/`AUTH_TEACHER_EMAILS` allowlist 및 관리자 사용자 관리 경로로만 부여한다. 한국어 이름/기관 NER, guardian/legal 최종 서명 동의서 evidence, 공개 Google OAuth 실제 `/turn` proof는 후속. **검증: `pytest app/ -q` 178 passed, `pytest engine_gateway/ -q` 11 passed, `npm run check:api-types`, `npm run typecheck`, `npm run build`, `npx playwright test e2e/auth.spec.ts --project=chromium-desktop --workers=1` 7 passed, `PLAYWRIGHT_PORT=5174 npm run e2e` 113 passed.**
- H4 온보딩·동의 게이트 3차: `app_user`에 이름·소속·학과·학년/직위·전화번호·주소/수령지·닉네임·자기소개·아바타 URL·약관/개인정보 동의 시각·버전 필드를 추가했다. 로그인 직후 `/onboarding` 완료 전에는 전역 `OnboardingGate`가 `/onboarding` 외 모든 앱 URL(`/learn`, `/settings`, `/admin`, `/dev/avatar-preview`, `/login` 포함)을 온보딩으로 이동시키고, 온보딩 중에는 공용 셸 메뉴 없이 가입 정보 입력 폼만 보여준다. learner `POST /sessions`와 voice dev persona 시작은 `onboarding_required`를 먼저 확인하고, 온보딩 저장 시 learner `consent_at`도 함께 세팅한다. 아바타는 `/users/me/avatar`에서 PNG/JPG/WebP MIME·시그니처·3MB 제한을 통과한 파일만 `USER_UPLOAD_DIR/profile-avatars`에 저장하고 DB에는 URL만 보관한다. 약관·개인정보 처리방침은 개인정보 보호법·처리방침 작성지침·안전성 확보조치 기준·약관규제법 취지를 반영한 개발 초안으로 `/users/legal-docs`에서 제공한다. 관리자/교수자 권한은 온보딩 화면에서 신청받지 않고 기존 `AUTH_ADMIN_EMAILS`/`AUTH_TEACHER_EMAILS` allowlist 및 관리자 사용자 관리 경로로만 부여한다. 한국어 이름/기관은 로컬 휴리스틱 마스킹과 합성 fixture 평가 harness 1차까지 완료했고, 모델 기반 정밀 ko NER, 실제 운영 말뭉치 기반 오탐/미탐 평가, guardian/legal 최종 서명 동의서 evidence, 공개 Google OAuth 실제 `/turn` proof는 후속. **검증: `pytest app/ -q` 178 passed, `pytest engine_gateway/ -q` 11 passed, `npm run check:api-types`, `npm run typecheck`, `npm run build`, `npx playwright test e2e/auth.spec.ts --project=chromium-desktop --workers=1` 7 passed, `PLAYWRIGHT_PORT=5174 npm run e2e` 113 passed.**
- RAG warm 동시성 안정화: layout/session E2E가 여러 세션을 빠르게 만들 때 BGE-M3 embedder가 동시에 여러 번 지연 로드되며 `tqdm` lock 예외와 API health/dev-login timeout이 반복됐다. `rag.py`에서 embedder load/encode를 process-wide `RLock`으로 직렬화하고, `sessions.py`의 `_warm_rag_caches`를 semaphore 1개로 제한해 warm task가 상담 요청 경로를 막지 않도록 했다. **검증: `pytest app/ -q` 125 passed, `npx playwright test e2e/layout-visual-gate.spec.ts e2e/session-layout.spec.ts --project=chromium-single-run --project=chromium-desktop --workers=1` 11 passed.**
- RAG warm 동시성 안정화: layout/session E2E가 여러 세션을 빠르게 만들 때 BGE-M3 embedder가 동시에 여러 번 지연 로드되며 `tqdm` lock 예외와 API health/dev-login timeout이 반복됐다. `rag.py`에서 embedder load/encode를 process-wide `RLock`으로 직렬화하고, `sessions.py`의 `_warm_rag_caches`를 semaphore 1개로 제한해 warm task가 상담 요청 경로를 막지 않도록 했다. **검증: `pytest app/ -q` 125 passed, `npx playwright test e2e/layout-visual-gate.spec.ts e2e/session-layout.spec.ts --project=chromium-single-run --project=chromium-desktop --workers=1` 11 passed.**
- 공개/로컬/Tailnet 로그인 복구: public API 530 원인은 prod에서 개발 전용 `VIGNETTE_VOICE_POC_SAMPLE_TTS=true`가 fail-close된 것과 DB `app.admin_engine_config` 기본 행 부재였다. 2026-06-27 모바일 502는 cloudflared 로그의 `127.0.0.1:8001` origin refused와 일치했다. `scripts/start-public-runtime.ps1`는 public prod 기동 시 샘플 TTS를 강제로 끄고, 운영 DB에는 `claude_cli`/`127.0.0.1:9099` engine config 행을 복구했다. 추가로 dev/Tailnet에서는 public OAuth callback이 로컬/Tailnet 세션으로 붙지 않으므로 Google 버튼과 직접 시작 URL을 `local_oauth_unavailable`로 차단하고 dev-login만 사용한다. public OAuth state는 HttpOnly 쿠키에 묶인 HMAC 서명 토큰으로 복구해 API 재시작 뒤 콜백이 `invalid_state`로 떨어지지 않는다. callback 실패는 비밀값 없는 reason/status로 남기고, 로그인 화면도 token/state/provider/identity 실패 메시지와 reason code를 분리한다. 2026-06-27 18:57 KST에 Tailnet stale Vite `allowedHosts` 재발 상태를 재확인했고, `alpaca-home.taile93291.ts.net`를 Vite 기본 허용 host에도 포함해 수동 Vite 기동 시 403 재발 가능성을 낮췄다. **검증: `https://vignette.chanpaca.net/login` 200, `https://api-vignette.chanpaca.net/health` prod/db/engine true, public Google auth redirect 302 + HttpOnly state cookie, provider callback error → `/login?oauth=access_denied`, `https://alpaca-home.taile93291.ts.net/login` 200 + `/api/health` dev/db/engine true + `/api/auth/config` 200, env 없는 임시 Vite host-header smoke 200, local/Tailnet Google direct는 `local_oauth_unavailable`, local auth E2E 7 passed, Tailnet auth/dev-login E2E 2 passed.**
- 공개/로컬/Tailnet 로그인 복구: public API 530 원인은 prod에서 개발 전용 `VIGNETTE_VOICE_POC_SAMPLE_TTS=true`가 fail-close된 것과 DB `app.admin_engine_config` 기본 행 부재였다. 2026-06-27 모바일 502는 cloudflared 로그의 `127.0.0.1:8001` origin refused와 일치했다. `scripts/start-public-runtime.ps1`는 public prod 기동 시 샘플 TTS를 강제로 끄고, 운영 DB에는 `claude_cli`/`127.0.0.1:9099` engine config 행을 복구했다. 추가로 dev/Tailnet에서는 public OAuth callback이 로컬/Tailnet 세션으로 붙지 않으므로 Google 버튼과 직접 시작 URL을 `local_oauth_unavailable`로 차단하고 dev-login만 사용한다. public OAuth state는 HttpOnly 쿠키에 묶인 HMAC 서명 토큰으로 복구해 API 재시작 뒤 콜백이 `invalid_state`로 떨어지지 않는다. callback 실패는 비밀값 없는 reason/status로 남기고, 로그인 화면도 token/state/provider/identity 실패 메시지와 reason code를 분리한다. 2026-06-27 18:57 KST에 Tailnet stale Vite `allowedHosts` 재발 상태를 재확인했고, `alpaca-home.taile93291.ts.net`를 Vite 기본 허용 host에도 포함해 수동 Vite 기동 시 403 재발 가능성을 낮췄다. **검증: `https://vignette.chanpaca.net/login` 200, `https://api-vignette.chanpaca.net/health` prod/db/engine true, public Google auth redirect 302 + HttpOnly state cookie, provider callback error → `/login?oauth=access_denied`, `https://alpaca-home.taile93291.ts.net/login` 200 + `/api/health` dev/db/engine true + `/api/auth/config` 200, env 없는 임시 Vite host-header smoke 200, local/Tailnet Google direct는 `local_oauth_unavailable`, local auth E2E 7 passed, Tailnet auth/dev-login E2E 2 passed.**
- Docker dev DB 안전장치: `scripts/dev-up.ps1`가 `vignette-dev-db` healthcheck/pg_isready/role safety를 점검하고, 새 컨테이너 생성 시 `vignette_owner`와 앱 role을 분리해 healthcheck를 붙인다. `infra\.env` compose 필수값 누락도 사전 경고한다. `scripts/dev-down.ps1`은 기본 DB 보존, `-Db` 지정 시 컨테이너 중지로 정리했다. **검증: 현재 Docker DB accepting connections, `vignette_app:false:false`, 기존 컨테이너 healthcheck 없음. 패키징 smoke에서 fresh compose DB는 owner/app role 분리 상태로 API healthy까지 통과했다. 기존 dev DB는 `POSTGRES_USER=vignette` 기반이라 운영 role 전환은 별도 owner migration 확인 후 진행한다.**
- Docker dev DB 안전장치: `scripts/dev-up.ps1`가 `vignette-dev-db` healthcheck/pg_isready/role safety를 점검하고, 새 컨테이너 생성 시 `vignette_owner`와 앱 role을 분리해 healthcheck를 붙인다. `infra\.env` compose 필수값 누락도 사전 경고한다. `scripts/dev-down.ps1`은 기본 DB 보존, `-Db` 지정 시 컨테이너 중지로 정리했다. **검증: 현재 Docker DB accepting connections, `vignette_app:false:false`, 기존 컨테이너 healthcheck 없음. 패키징 smoke에서 fresh compose DB는 owner/app role 분리 상태로 API healthy까지 통과했다. 기존 dev DB는 `POSTGRES_USER=vignette` 기반이라 운영 role 전환은 별도 owner migration 확인 후 진행한다.**
The DB also stored fast-loop `turn_client_state` rows for both sessions. The full JSON output is `docs/ops/resistance-openness-db-smoke-2026-06-28.json`.
The DB also stored 10 `app.turns` rows and fast-loop `turn_client_state` rows for both sessions. The script now asserts the full API curve, stage trajectory, DB turn row count, and client-state persistence before writing `ok: true`. The full JSON output is `docs/ops/resistance-openness-db-smoke-2026-06-28.json`.