vignette/docs/ops/backlog-2026-06-26.md
Yun Chan 4e6b0045e3 관리자 워크스페이스 접근 확장과 소유자 결정 7건 확정 반영
- 관리자(role=admin)가 학습자·교수자·관리자 워크스페이스를 모두 접근하도록
  can_access_role/require_role와 프론트 auth 헬퍼·Sidebar 내비를 정리하고
  admin 워크스페이스 내비 E2E를 추가.
- 소유자 결정 7건 전건 확정(2026-06-30)을 SSOT 대시보드·백로그에 반영하고
  결정 필요 7→0으로 동기화. SSOT drift 게이트 기대 카운트도 갱신.
- 확정된 H1 평가설계(κ≥0.70·ICC≥0.75·환각률≤0.03·t-검정 α=0.05·무작위 배정)를
  approved-export κ 게이트(checker·dataset_export·recursive export)와
  KPI 측정계획·export manifest 문서에 반영.

검증: npm run typecheck, npm run check:api-types, 백엔드 pytest 290 passed,
admin 내비 E2E 1 passed, 레이아웃 시각게이트 9/9, session-layout 4 passed,
SSOT drift 게이트 PASS.
2026-06-30 10:57:46 +09:00

72 KiB
Raw Blame History

Vignette 백로그 — 2026-06-26

이 세션(레이아웃 시각 수용 마감 + live 운영 증거 라운드) 이후 남은 항목을 한곳에 모은다. 대시보드(docs/dev_dashboard.html)의 상태와 일치한다. 운영 원칙상 가짜 증거로 DONE 표기하지 않으므로, 실증/외부 의존/소유자 결정이 필요한 항목은 여기서 추적한다.

최신 동기화(2026-06-29): M2 다회기 케이스 아크는 TurnMemory 값 객체로 턴 메모리 전달 경계를 줄였고, DigestQualityAssessment/SessionDigestWorkerOutcome로 LLM digest 후보 local quality harness를 추가했다. 이번 라운드에서는 session_digest_worker.pyCompressionJob→Node-compatible GenerateRequest 변환, 주입형 engine/audit 호출, accepted-only session_summary/case_profile 적용 계획과 one-shot DB loader 경계를 소유한다. scripts/run-session-digest-worker.py는 metadata-only dry-run/apply runner를 제공하고, API는 SESSION_DIGEST_WORKER_ENABLED=false 기본값에서 opt-in일 때만 세션 종료 뒤 background worker를 예약한다. loader/apply는 compressed_by IS NULL CAS로 이미 압축된 세션 재실행 race를 막고, engine 호출은 DB transaction 밖에서 수행한다. loader는 persisted fallback summary와 client-visible text_masked transcript만 재구성하며 raw text, evaluator-only turn, CCD, end_state를 압축 prompt에 넣지 않는다. scripts/check-dev-dashboard-ssot.py는 대시보드 카드 상태와 M2 30/87 검증 수치, stale DONE/GATE 문구를 guard한다. recall_summary/pinned_facts/recent_turns/kb_behavior_cuesTurnContext.memory에 보관되고, prepare_turn(memory=...)에서 마스킹된 뒤 build_turn_messages(memory=...)로 전달된다. auth managed-user upsert 입력도 ManagedUserUpsertInput으로 단일화했지만, 이는 내부 호출 경계 정리이며 외부 API·온보딩 정책·권한 모델 변경은 아니다. Python engine gateway의 provider/model 응답 메타 규칙은 helper로 모았고, GatewayPromptParts가 current-turn system_prompt/user_payload 분해 경계를 소유한다. Node conformance runner는 gateway-default가 request default-routing sentinel이며 response/done model은 resolved provider model이어야 함을 검증한다. 검증은 py_compile, runner --help, dashboard SSOT checker PASS, M2 digest worker + memory focused 30 passed, M2 주변 회귀 87 passed, M2 local harness 20 passed, M2/voice/evaluation focused 115 passed, auth/admin/session focused 103 passed, gateway contract 27 passed, backend focused 120 passed, Node conformance OK, npm run check:api-types. 단, 실 provider 장시간 운영·임상 골든셋 품질평가·재압축은 계속 후속 GATE다.

최신 동기화 추가(2026-06-29): 관리자·교수자 메일링 시스템 1차가 추가됐다. 가입 승인 요청과 회기 검토 요청은 app.notification_event/app.notification_delivery에 idempotent 큐로 남기고, SMTP 설정이 있을 때만 실제 메일을 발송한다. 메일 본문은 Vignette 톤앤매너를 따르되 축어록·평가 전문을 포함하지 않고 /admin/users, /teach/session/:sessionId/review 딥링크만 제공한다.

분류: 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 사례개념화 산출물은 저장형 워크시트, 외부 루브릭 scaffold, 교수자 수동 검수 상태 저장까지 4차 구조를 만들었고, C2 위기개입 프로토콜은 1차 구조, C3 이론모드는 2차 명시 선택 UI까지 만들었다. 콘텐츠 정의는 임상팀(구훈정·어유경) 소유라 코드는 구조를 선제 구축하되 임상 문안과 평가기준은 외부 정의로 받는다.


B1. 비차단 코스메틱 폴리시

  • 공용 셸 사이드바 구분선 전체 높이 — (2026-06-26 처리) .vg-nav border-right(sticky·뷰포트 높이)를 제거하고, .vg-shell__body 컨테이너 배경 하어라인(linear-gradient 1px×100%, --nav-cur로 그리드·구분선 폭 동기화)으로 본문 그리드 전체 높이를 따라가는 세로 구분선을 그린다. 1024 축소폭은 --nav-cur: var(--nav-w-collapsed)로, 720 이하(상단 가로바 전환)는 background-image:none으로 정합. components/shell/shell.css. 검증: npm run typecheck PASS + vite build PASS. 시각 회귀 게이트(gate 7/7 + 포커스 E2E + session-layout 8/8)는 web+api+DB 스택(Docker 미기동·secrets 부재)으로 이 환경 미실행 — 스택 가용 시 1회 시각 확인 권장.
  • 세션 종료 UX와 dark theme SSOT — (2026-06-27 처리) 오발동과 모바일 조작 불안을 줄이기 위해 세션 하단의 드래그형 SlideToEnd를 명시 확인 다이얼로그로 교체했다. Topbar/Settings의 vignette.theme 읽기·저장·data-theme 적용은 apps/web/src/lib/theme.ts 단일 경로로 통합했고, 저장값이 없으면 dark 기본값을 부팅 전에 적용한다. API 기본 preference system은 Settings에서 light로 오해하지 않고 현재 초기 테마를 따른다. 회기 리뷰·설정 포함 주요 페이지의 흰 섹션 잔재도 dark 작업면으로 맞췄다. 검증: npm run typecheck, npm run build, npx playwright test e2e/layout-visual-gate.spec.ts e2e/session-layout.spec.ts e2e/avatar-expression.spec.ts e2e/session-review.spec.ts e2e/settings.spec.ts --project=chromium-desktop --project=chromium-mobile --project=chromium-single-run --workers=1 36 passed. 추가 시각 검사: 7개 주요 화면 x 3뷰포트 캡처 21장, horizontal overflow 0, 큰 흰 CSS 배경면 0.
  • P1 세션 아바타 PSD v2 정렬 — (2026-06-27 처리) 컨셉 보드 크롭 p1-concept를 기본값에서 내리고, 사용자 제공 라투디 여캐_ver2.psd에서 추출한 seoyeon-live2d-psd-v2를 P1 기본값으로 연결했다. sad 표정은 PSD의 울상 눈썹, 우는 입, 눈물 파츠를 별도로 합성하고, 데스크톱 스테이지는 3행 구조를 유지해 현재 상태 배지가 아바타와 겹치지 않게 했다. 검증: PSD 파츠 추출 53개, rasterArtSet="seoyeon-live2d-psd-v2", sad 프리뷰에서 눈물/우는 입/울상 눈썹 확인.
  • 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회 확인 권장.
  • 학습자·교수자·운영·페르소나 스튜디오 레이아웃 정렬 — (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.
  • 관리자 권한 위임 1차 — (2026-06-28 처리, 2026-06-29 보강) 역할(role)과 관리자 페이지 진입권(app_user.admin_access)을 분리했다. AUTH_SUPER_ADMIN_EMAILS 기본값은 yunchan@twentyoz.kr, hoonjungkoo@hs.ac.kr이며 슈퍼 관리자만 /admin/users에서 관리자 권한을 부여·회수한다. 실제 admin 역할 사용자는 학습자·교수자·관리자 라우트 전환이 가능하고, /admin 왼쪽 메뉴도 운영 홈/사용자/권한/티켓과 교수자·학습자 작업 공간을 함께 노출한다. 일반 학생/교수 계정은 admin_access=true여도 관리자 콘솔 진입권만 얻고 학습자·교수자 역할권은 추가되지 않는다. 검증: python -B -m pytest -p no:cacheprovider app/test_auth_providers.py app/test_runtime_policy.py app/test_rbac_idor.py app/test_teacher_dashboard.py app/test_learner_dashboard.py -q 70 passed, python -B -m pytest -p no:cacheprovider app/test_auth_providers.py app/test_admin_ops.py app/test_runtime_policy.py -q 64 passed, npm run check:api-types, npm run typecheck, npm run build, npx playwright test e2e/admin.spec.ts --project=chromium-desktop --workers=1 8 passed.
  • 관리자·교수자 메일링 시스템 1차 — (2026-06-29 처리) 신규 외부 로그인 사용자가 account_status=pending이면 관리자 승인 메일 큐를 만들고, 회기 종료 후 평가 또는 error record가 저장되면 담당 교수자/관리자에게 회기 검토 메일 큐를 만든다. app.notification_eventaccount_pending_approval:{user_id}, session_review_ready:{session_id}, admin_test_email:{uuid} idempotency key로 중복 발송을 막고, app.notification_delivery는 수신자별 queued/sending/sent/failed/skipped 상태와 재시도 정보를 보관한다. 테스트 메일은 대량 오발송을 피하기 위해 AUTH_SUPER_ADMIN_EMAILS 대상에게만 큐잉한다. SMTP 설정은 NOTIFICATION_EMAIL_PROVIDER=smtp, SMTP_* env로 주입하며, 관리자 API GET /admin/notifications, POST /admin/notifications/process, POST /admin/notifications/testscripts/run-notification-worker.py로 큐를 확인·처리한다. HTML 메일은 Vignette 종이 배경/세이지 CTA 톤을 inline style로 유지하고, 본문에는 축어록·평가 전문을 넣지 않는다. 검증: python -m compileall apps\api\app\services\notifications.py apps\api\app\routes\admin.py apps\api\app\routes\sessions.py apps\api\app\auth_sessions.py apps\api\app\routes\users.py, python -B -m pytest -p no:cacheprovider app/test_notifications.py app/test_auth_providers.py app/test_admin_ops.py app/test_runtime_policy.py -q 69 passed, npm run generate:api-types, npm run check:api-types, npm run typecheck, npm run build, python scripts\check-deploy-preflight.py --env-file infra\.env.example --allow-placeholder-secrets --skip-db, python scripts\run-notification-worker.py --help, python -X utf8 scripts\check-dev-dashboard-ssot.py --json PASS.
  • 회기 아카이브 저장/복원 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.
  • 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.
  • 빈상태 컬럼 높이 여백 — (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.
  • 다크 UI v2 시안 실제 적용 라운드 — (2026-06-29 처리) 02/03/04-*-v2-dark-unified.png 시안을 실제 React/CSS 화면으로 반영했다. P1 라이브 상담 세션은 내담자 무대 발화, 좌측 세션 진행, 우측 라이브 신호/안전 점검을 다크 훈련 화면으로 재구성했다. P2 회기 리뷰는 learner filled 상태를 3열 workbench(요약/축어록/평가 rail + 하단 워크시트)로 바꾸고 empty review는 sparse third column 없이 유지했다. P3 학습자 홈은 대시보드에서 연습 대상 레일을 제거하고, 진행 회기/리뷰 대기/최근 평가/라포 흐름을 상단 CTA 바로 올렸으며, 오늘 이어갈 회기·AI 코치·다음 연습 추천·최근 피드백을 하나의 lh-work-cluster 작업 묶음으로 재배치했다. 오른쪽 레일은 최근 기록, 리뷰 대기, 반복 대상만 담당한다. 생성 PNG는 앱에 직접 넣지 않고 레이아웃·밀도·톤 기준으로만 사용했다. 새 실제 렌더 캡처: docs/design-verification/full-pages/02-learner-home-{desktop,tablet,mobile}.png, 03-session-active-{desktop,tablet,mobile}.png, 04-session-review-{desktop,tablet,mobile}.png, 04-session-review-empty-{desktop,tablet,mobile}.png. 실행 기록은 docs/ops/dark-ui-refresh-implementation-plan-2026-06-28.md에 고정했다. 최신 검증: npm run typecheck, npm run build, npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run --workers=1 9 passed.
  • 운영 티켓·헬스 샘플 이력 저장 모델 1차/admin 운영 홈과 /admin/ticketsapp.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.
  • 사용자별 운영 티켓 조회 UI — (2026-06-28 처리) 학습자 Settings에 지원 요청 섹션을 추가해 본인이 접수한 티켓의 상태·카테고리·우선순위·담당 그룹·해결 메모를 읽기 전용으로 확인한다. GET /users/support-ticketsreporter_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.
  • 상시 synthetic monitor/cron 샘플러 1차 — (2026-06-28 처리) /admin/health와 같은 헬스 계산을 record_admin_health_sample()로 공통화하고, 브라우저 세션 없이 scripts/record-admin-health-sample.pyapp.admin_health_event에 서비스별 샘플을 append한다. Windows 예약 실행은 scripts/install-health-sampler-task.ps1VignetteAdminHealthSampler 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.
  • 헬스 이벤트 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차 — (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.
  • 운영 티켓 중복 저장·수동 연결 1차 — (2026-06-28 처리) app.support_ticket에 결정론적 fingerprintparent_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/작업 스레드 생성, 처리 결과 audit trail 확장은 아직 설계/승인 필요. 담당 그룹 자동 배정·우선순위 escalation·raw/rollup 보존기간 같은 운영 정책은 B3 결정 항목에서 먼저 닫아야 한다. 자동 수정은 운영자 승인 전까지 실행하지 않는다.
  • 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(현재 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), M2 route-level seed recall 주입(py_compile, M2 focused 68)을 확인했다. 최신 다크 UI v2 라운드는 learner.spec.ts+session-review.spec.ts 9 passed, session-layout.spec.ts 8 passed, layout-visual-gate.spec.ts 9 passed로 별도 기록한다.


B2. 환경 제약 — 이 워크스테이션에서 증거 생산 불가

  • vnet.18ka.net 공개 live 전환 — 코드/스크립트는 vnet.18ka.net·api-vnet.18ka.net을 허용하도록 갱신 완료. Tailnet 주소 https://alpaca-home.taile93291.ts.net은 dev-login까지 검증됐고, public OAuth redirect가 로컬/Tailnet 세션으로 붙지 않는 혼선을 막기 위해 Tailnet Google 버튼과 직접 Google 시작 URL은 local_oauth_unavailable로 차단한다. 공개 vnet은 현재 (1) vnet.18ka.net/api-vnet.18ka.net DNS 부재, (2) cloudflared 현재 인증 컨텍스트가 18ka.net zone이 아니라 chanpaca.net zone에 route dns를 생성, (3) Google Console redirect URI https://api-vnet.18ka.net/auth/callback 등록 필요 때문에 live 완료 아님. 현재 prod API 8001 자체는 db:true, engine:true로 건강하다. 상세: docs/ops/tailscale-vnet-runtime-2026-06-27.md.
  • 실배포 infra/.env owner-secret fill-in + real preflight — compose 템플릿과 더미 env smoke는 통과했지만 실제 배포지는 소유자 비밀값이 필요하다. 필수값: APP_DB_PASSWORD, OAUTH_GOOGLE_CLIENT_ID, OAUTH_GOOGLE_CLIENT_SECRET, OPENAI_API_KEY, SESSION_SECRET, production-safe engine/voice flags. 현재 로컬 stray 값(ENGINE_MODE=claude_p, prod sample TTS flag 등)은 운영값으로 복사하면 안 된다. 실제 secret 주입 후 scripts/check-deploy-preflight.py DB 포함 모드와 배포지 health를 별도 증거로 닫는다.
  • 공개 Google OAuth 실제 /turn proof — 로그인 가능한 계정으로 storageState 캡처 후 E2E_PUBLIC_AUTH=1 + chromium-public-auth 1회 통과 필요. (소유자 지시로 이번엔 보류.)
  • 음성 캐스케이드 live — (2026-06-29 갱신) 로컬 API에서 실제 OpenAI STT/TTS 경로는 live WS smoke로 통과했다(P2 세션, MP3 발화 → transcript 52자 → reply 105자 → TTS binary 66 chunks/266,496 bytes). Session UI는 Web Audio unlock 재생과 AudioWorklet-first PCM16 capture로 보강했고, backend는 format=pcm raw chunk를 STT 업로드 직전 WAV로 정규화한다. /voice/ws는 provider-neutral stt_result control을 받아 EOT ready/pending을 route에서 판단한다. synthetic mic E2E는 AudioWorklet 경로로 통과했다. 남은 범위는 실제 Deepgram interim/final WSS, 물리 마이크, 공개 WSS, 50분 양방향 장시간 실측이다.
  • claude_cli ↔ Messages API 폴백 동일성ANTHROPIC_API_KEY가 있어야 Messages API 경로를 돌려 동일성 검증 가능. claude_cli 경로는 게이트웨이 probe로 live 실측 완료.
  • 저항엔진 openness 곡선 DB 실증 — (2026-06-28 처리) scripts/smoke-resistance-openness-db.py로 실제 API 경로(dev-login→onboarding→P1 세션 2개→각 5턴)를 실행하고 DB app.session_state/app.turns/app.turn_client_state를 직접 조회했다. 공감 5턴은 API curve 0.0→0.0→0.095→0.133→0.171, DB final stage=탐색, effective_openness=0.17100003361701965; 조언점프 5턴은 API curve 전부 0.0, DB final stage=라포, effective_openness=0.0, resistance=0.9580000042915344. 증거: docs/ops/resistance-openness-db-smoke-2026-06-28.json, docs/ops/resistance-openness-db-smoke-2026-06-28.md. 검증: python -B -m py_compile scripts/smoke-resistance-openness-db.py, pytest app/test_state_machine_resistance.py app/test_evaluation_persistence.py -q 9 passed, live smoke PASS.
  • 재부팅 후 watchdog smokewatch-public-runtime.ps1 + Scheduled Task가 재부팅 후 엔진/API/터널을 복구하고 public /turn이 통과하는지 실측. 재부팅 불가로 미실행(parser/ check-only 경로는 확인됨). 2026-06-27 점검에서 아직 DNS가 없는 api-vnet.18ka.net은 기본 watchdog 검사에서 제외했고, 개통 후 -AdditionalPublicHealthUrls로 명시 추가하도록 runbook을 보강했다.
  • 상주 엔진풀 RSS 실측 — (2026-06-28 처리) gateway 세션 1개를 열고 claude -p 자식 프로세스 RSS를 Windows WorkingSet 기준으로 계측했다. health.sessions=1 상태에서 gateway python.exe PID 51592는 after-turn RSS 16.2MB, 상주 claude.exe PID 51688은 RSS 363.3MB/Private 628.8MB였다. Claude CLI plugin runtime bun.exe 자식 2개는 합계 RSS 178.6MB였다. 세션은 측정 직후 DELETE /session/{id}로 종료했다. 증거: docs/ops/engine-rss-smoke-2026-06-28.md.

B3. 소유자(윤찬) 결정 필요 — 단독 결정 시 월권

결정되면 대시보드 결정 필요 탭과 관련 Phase 항목을 갱신한다.

  • 운영 티켓·가용성 정책 결정 — 결정됨(2026-06-30): 헬스 raw 90일·rollup 365일 보존, 우선순위 escalation은 운영자 수동 승인, SLA 미표기(관측 샘플 이력 유지), 담당 그룹 자동 배정 미도입. 후속: retention 기본값(raw 90/rollup 365) 적용·검증. escalation/그룹 자동화는 미도입으로 고정.
  • 평가설계 (H1: 실험/통제군·3척도·κ/ICC) — 결정됨(2026-06-30): 평가자 신뢰도 κ≥0.70 & ICC≥0.75, 환각률 ≤3%, 독립표본 t-검정(α=0.05), 20명 실험/통제군 무작위 배정, 단회기 50분·3척도 pre-post 유지. 후속: KPI 측정계획·report threshold에 κ/ICC/환각률 반영. 실제 20명 파일럿 evidence·통계해석·공식 효과성 판정은 별도 GATE.
  • s2s 2차 PoC 채택 여부 — 결정됨(2026-06-30): s2s 2차 PoC 유지. 캐스케이드 1차를 운영 기준으로 두고 docs/decisions/voice-s2s-poc.md 비교 PoC를 계속 연다. 최종 채택/폐기는 비교 리포트 이후. 후속: s2s 비교 PoC 구현·레이턴시/안전/감사 실측.
  • 서비스용 페르소나 음성 모델·권리 정책 — 결정됨(2026-06-30): 상업 라이선스가 명확한 대체 TTS provider 채택. Higgs v3 무참조 synthetic 샘플과 VIGNETTE_VOICE_POC_SAMPLE_TTS=true dev sample provider는 개발 플래그로만 유지하고, 실존 인물/성우 reference 사용 금지를 유지한다. 후속: 대체 TTS provider 선정·통합, dev/서비스 경로 분리 유지.
  • fast-loop 외부 API 허용 — 결정됨(2026-06-30): 외부 폴백 허용. 로컬 상주 분류기 기본 + 외부 Haiku·Solar 폴백, 외부 전송 시 데이터주권·로그 보존·동의 가드 적용. 후속: 폴백 경로·전송 가드·관측 구현, 로컬 우선/외부 폴백 라우팅 정책 명시.
  • 재귀학습 fine-tuning 범위 — 결정됨(2026-06-30): fine-tuning 포함. few-shot 자동갱신에 더해 fine-tuning까지 범위에 두되, 데이터셋은 동의 범위·추가 축어록 수급 결정이 닫힌 뒤 구성한다. few-shot 자동갱신은 선행 가능.
  • 자유연습 기본값 — 결정됨: 기본 ON(+피드백 권장 배지+교수자 토글). 미동의·미성년 개인정보 gate만 차단한다. 근거: docs/redteam/MASTERPLAN_REVISIONS.md F-10 승인.
  • 추가 축어록 수급 규모·일정 — 결정됨(2026-06-30): 20건을 1개월 내 수급. 후속: 실제 수급·동의·정제는 임상/한신대 거버넌스 게이트와 연계.
  • 백엔드 언어 방향 — (2026-06-28 결정, 2026-06-29 보강) Node.js를 장기 교체 대상과 신규 분리 서비스 우선 스택으로 삼되, 현재 FastAPI 전면 재작성은 납품 일정 리스크라 보류한다. 전환은 계약 우선 strangler 방식으로 진행한다. 고정된 내부 경계는 app/contracts/engine_gateway.py의 gateway request/response/SSE 계약과 gateway-default sentinel, structured_payload_from_response(), scripts/check-engine-gateway-contract.mjs schema-driven Node conformance runner, GatewayPromptParts, app/session_read_model.py, app/persona_read_model.py, app/persona_generation_contract.py, SessionEvaluationWrite.from_result()/from_error()다. FastAPI route는 auth/RLS DB read/persistence, teacher/admin gate, RAG/LLM side effect, session lifecycle, provenance/error mapping을 계속 소유한다. 2026-06-29 후속으로 Python gateway의 provider/model 응답 메타 규칙을 helper로 모으고, Node runner가 gateway-default request sentinel과 resolved response model 분리를 검증하며, current-turn prompt split과 missing-user 400 경계를 테스트로 고정했다. 결정 기록: docs/decisions/backend-node-transition.md. 검증: node scripts/check-engine-gateway-contract.mjs --json, gateway contract 27 passed, persona contract+review 39 passed, backend focused 120 passed, npm run check:api-types, npm run typecheck.
  • Node 전환 경계 보강 — 페르소나 생성 계약 + 평가 저장 packet — (2026-06-29 처리) app/persona_generation_contract.py가 페르소나 draft structured schema, prompt bundle id/version/hash, GenerateResponse payload extraction, generated draft coercion/default fallback을 소유한다. SessionEvaluationWrite.from_result()/from_error()app.session_evaluation 저장 packet(status/source/scope/stage/payload/error) 생성을 소유한다. Node conformance runner는 stream event 목록과 payload 검증을 engine_gateway_schema.v1.jsonStreamPacket.oneOf, payload $ref, x-engine-gateway-sse-events에서 읽는다. routes/personas.py는 auth, source 등록·조회, RAG evidence, engine 호출, provenance 조립, HTTP error mapping을 유지하고, routes/sessions.py/routes/eval.py는 factory로 named packet을 만들어 저장한다. 검증: py -3.11 -X utf8 -B -m py_compile app\persona_generation_contract.py app\routes\personas.py app\persona_read_model.py app\test_persona_generation_contract.py app\test_persona_review.py engine_gateway\test_gateway_model.py app\session_persistence.py app\routes\sessions.py app\routes\eval.py app\services\evaluator.py app\services\live_coach.py, py -3.11 -X utf8 -B -m pytest -p no:cacheprovider app\test_persona_generation_contract.py app\test_persona_review.py -q 39 passed, backend focused 120 passed, node scripts\check-engine-gateway-contract.mjs --json, npm run check:api-types, npm run typecheck.

B4. 외부 거버넌스 — 한신대/데이터 steward 서면 증거

  • 한신대 데이터/SSO 거버넌스 게이트 — SSO claim mapping, 추가 축어록 수급, 미성년 원본 활용동의, 개인정보 처리방침. 로컬 문서 골격 docs/ops/hanshin-data-governance-gate.md 준비됨, 외부 서면 증거 필요. (소유자 지시로 이번엔 보류.)
  • L1 스택 정합성·저작권 등재 문서 반영 — 백엔드 방향은 Node.js 교체 가능성 우선으로 결정됐지만, 신청서/저작권 등재 문서에는 현재 FastAPI 유지 사유와 계약 우선 전환 계획을 반영해야 한다. 로컬 결정 기록은 docs/decisions/backend-node-transition.md; 외부 제출/등재 증거가 필요하다.

Phase 3 파일럿 게이트 (실참여자 필요)

  • 20명 교육용 파일럿 운영 / 효과성·KPI 측정(SUS·자기효능감·κ/ICC·환각률) / 재귀학습 데이터셋 approved 산출 / 개인정보·동의 감사. 문서/checker와 dry-run exporter는 준비됨(docs/phase3/*, scripts/check-phase3-artifacts.py, scripts/export-recursive-dataset.py). 2026-06-27 checker 강화로 CSV enum, KPI metric required fields, approved export의 PII pass·κ/ICC·withdrawn exclusion·consent scope·file sha256 false-positive 차단을 추가했다. 2026-06-28에는 3척도 pre/post aggregate evidence 원장 app.learner_prepost_measure, 학습자 본인용 GET/PUT /users/me/prepost-measures, SessionReview 입력 UI를 추가했고, app.services.phase3_kpi_export/scripts/export-phase3-kpi.pyprepost_measures.csvkpi_report.json scaffold를 생성한다. participant id는 가명화하고 3척도 paired normalized delta, complete/missing pair를 계산한다. phase3_kpi_contract.py는 KPI metric 이름·필수키·computed_prepost/design_pending status 값을 공유해 exporter/checker/test drift를 줄이고, checker는 status unknown 값을 error로 처리한다. 공식 문항 확정, 통계 검정, 실험/통제군 배정, 실제 approved evidence는 파일럿·steward/legal 검수·IAA 통과 시점. 검증: py -3.11 -X utf8 -m py_compile apps\api\app\services\phase3_kpi_contract.py apps\api\app\services\phase3_kpi_export.py apps\api\app\test_phase3_kpi_export.py apps\api\app\test_phase3_artifact_checker.py scripts\check-phase3-artifacts.py scripts\export-phase3-kpi.py, py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_phase3_kpi_export.py app/test_phase3_artifact_checker.py -q 10 passed, py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_user_support_tickets.py app/test_runtime_policy.py app/test_admin_ops.py app/test_phase3_kpi_export.py app/test_phase3_artifact_checker.py -q 46 passed, py -3.11 -X utf8 scripts\export-phase3-kpi.py --help, py -3.11 -X utf8 scripts\check-phase3-artifacts.py --help.

이번 세션에 닫은 것(참고)

  • 레이아웃 시각 수용: 기본 게이트 7/7 + 적대적 재검수 7/7 accept에 더해 빈 회기리뷰 전용 gate를 추가한 현재 9/9 — DONE.
  • live 운영 증거: engine config 운영값, 상주 엔진풀 probe(TTFT/cost/세션재사용), 상주 엔진풀 RSS 실측, Postgres RLS/audit smoke 5 checks PASS, turn cost telemetry(app.turns 13행) — DONE.
  • P1 서연 음성 아트 PoC: Higgs v3 무참조 synthetic seed + 5개 정서/속도 변주(mp3/wav) 생성. 실존 reference voice 미사용. /voice/ws에는 dev-only sample TTS provider로 연결했고 live text_turn smoke에서 p1-sample-poc binary 20 chunks/78,573 bytes 수신 확인. 이 provider는 TTS override만 담당한다(마이크/STT는 OpenAI 키 필요). 프로덕션 탑재는 라이선스/권리 결정 전 금지.
  • Session.tsx 음성 guard/view-model P2 리팩터 + capture/EOT 계약 보강: isVoiceStatusBusy()sessionVoiceStatusView()가 마이크 busy/disabled, aria label, transcript/response/status label, 텍스트 입력 차단 계산을 소유한다. Session mic capture는 AudioWorklet-first PCM16 + MediaRecorder fallback이고, /voice/wsstt_result control로 EOT ready/pending을 route에서 판단한다. 검증은 web typecheck/build, session-mvp, voice-success synthetic UI, session-layout, test_voice_ws.py/test_voice_service.py 범위만 인정한다. 실제 Deepgram WSS, 물리 마이크·공개 WSS·50분 장시간 실측은 B2 음성 캐스케이드 live 항목에 계속 남긴다.
  • 부수: RLS smoke 스크립트 cleanup RLS 컨텍스트 버그 수정, vignette_app(NOBYPASSRLS) dev 역할 프로비저닝.
  • 평가 SSOP 2차: TurnRecord.evaluationapp.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 + 학습자 리뷰 3열 workbench: 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 충돌을 피한다. 2026-06-28 refactor-governance P4에서는 live turn의 프로세스 로컬 source pack snapshot은 유지하되 관리자 sync/CLI가 refresh=True로 캐시를 비우고 repo 파일을 다시 읽게 해 stale content_hash 비교를 막았다. 학습자 SessionReview 데스크톱은 요약/흐름, 축어록, 평가 rail의 3열 workbench와 하단 워크시트로 재배치했고, empty review는 2열 이하로 유지한다. 2026-06-29에는 워크시트/pre-post/교수자 메모 입력에 name/autocomplete/aria-label과 공유 focus token을 보강하고, 발화 이동 버튼 aria-label을 추가했다. 검증: 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 py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_live_coach_sources.py -q 9 passed, H2 related py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_live_coach_sources.py app/test_orchestrator_masking.py app/test_session_turn_persistence.py -q 44 passed, py -3.11 -X utf8 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. 3열 workbench 검증: npm run typecheck, npm run build, 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, 04-session-review-desktop.png 직접 확인. 로컬 DB smoke: sync 4 sources/15 chunks embedded, 재호출 skipped_unchanged 4, official_suicide_risk_guidelines source_id 필터 검색 성공.
  • 계약 SSOT 7차: 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에 고정했고, 69차에서는 ReviewPhaseSegment.key/label, SessionReviewResponse.reachedPhase, EvaluationSummary.stage, 평가 트리거 응답 stage, TeacherSessionSummary.stage, TeacherGrowthPoint.stage도 union/null 계약으로 축소했다. 71차에서는 공개 공유 응답 PublicSessionShareResponse.reachedPhase와 live-coach 이력 LiveCoachEvent.stageStageLabel | null 계약으로 축소했다. 72차에서는 app/stage_contract.pyStageLabel, ReviewPhaseKey, legacy stage code 정규화, unknown→null 판단, 리뷰 phase key 매핑을 소유하게 해 turn_runtime.py, session_read_model.py, routes/eval.py의 중복 stage 규칙을 제거했다. generated optional/default 차이는 UI form state, notification default helper, 배열 렌더링 fallback, live-coach 단계 미상 표시로 흡수한다. .github/workflows/api-contract.yml은 PR/master push에서 npm run check:api-types drift를 막는다. 검증: py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_evaluation_persistence.py app/test_evaluator_model_routing.py app/test_teacher_dashboard.py app/test_rbac_idor.py app/test_session_turn_persistence.py -q 50 passed, py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_session_share.py app/test_live_coach_sources.py app/test_session_turn_persistence.py app/test_orchestrator_masking.py -q 43 passed, py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_stage_contract.py app/test_session_share.py app/test_session_turn_persistence.py app/test_evaluation_persistence.py app/test_teacher_dashboard.py -q 42 passed, py -3.11 -X utf8 -m py_compile apps/api/app/stage_contract.py apps/api/app/turn_runtime.py apps/api/app/session_read_model.py apps/api/app/routes/eval.py apps/api/app/routes/sessions.py apps/api/app/routes/share.py apps/api/app/services/live_coach.py, npm run check:api-types, npm run typecheck, npm run build, 과거 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.
  • 교수자 성장 추적/리뷰 상세 진입 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.services.session_metrics로 공용화하고, GET /sessions/dashboard가 본인 세션 기반 overview/growth/persona_progress/achievements/recent_feedback를 반환한다. 학습자 홈은 누적 회기, 리뷰 대기, 최근 평가, 라포 흐름, 페르소나별 진행, 최근 피드백, 마지막 세션 리캡을 실제 서버 데이터로 표시한다. 성취는 공식 등급/수료가 아니라 실제 연습 milestone만 사용한다. 2026-06-29 refactor-governance 패스에서 session_metrics.turn_technique_label()이 DB 재수화 계약의 label_ko를 우선하고 기존 label/name/id/code fallback을 유지하도록 고정했다. 검증: py -3.11 -X utf8 -B -m pytest -p no:cacheprovider app/test_evaluation_persistence.py app/test_evaluator_model_routing.py app/test_session_turn_persistence.py app/test_rbac_idor.py app/test_teacher_dashboard.py app/test_learner_dashboard.py -q 59 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 저작 10차: 교수 콘솔의 JSON 패널을 분리해 teacher/admin 전용 /teach/personas 3열 스튜디오(좌: 저작 흐름·카탈로그·검수 큐, 중앙: RAG 자료 등록·항목형 편집 탭, 우: 검증·RAG 근거·검수 결정)로 옮겼다. 2026-06-27 UI 정리에서 중복 네이티브 파일 입력을 커스텀 SSOT 첨부 드롭존으로 교체하고, 저작 스테퍼·탭별 작성 가이드·검증 dot 정렬을 보강했다. 2026-06-28 항목형 목록 UI 정리에서 자동사고, 회기 시나리오, 말투 filler/verbal tic/nonverbal cue, 역린·금기 응답·금기어를 행 추가/삭제 UI로 전환했고, 저장 payload는 기존 배열 schema를 유지하며 빈 항목만 저장 직전에 제거한다. 프롬프트 탭은 raw JSON textarea 대신 L1 카드·인적 범주·임상 배경·말투·수치 파라미터·역린·회기 시나리오·추가 계약 섹션으로 같은 draft 데이터를 라벨형 검토 UI에 표시한다. 레이아웃 근거와 생성 시안은 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/generatesource_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.pyapp.services.source_pack_sync는 repo-managed source pack의 active content_hash를 DB에서 비교하고, 변경 시 kb.document.version을 최신+1로 색인한다. 교수자는 공개 목록에서 승인본을 다음 버전 draft로 복제해 수정하고, 불필요한 페르소나는 기존 회기 FK 보존을 위해 같은 code family 전체를 archived로 보관 처리한다. app/persona_read_model.py는 catalog/review/draft/source/evidence DTO와 mapper를 route에서 분리해 schema 이름과 source/degraded fail-closed 경계를 유지한다. 암호화 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, python -B -m py_compile app/persona_read_model.py app/routes/personas.py, python -B -m pytest -p no:cacheprovider app/test_persona_review.py -q 33 passed, python -B -m pytest -p no:cacheprovider app/test_persona_review.py app/test_session_turn_persistence.py -q 55 passed, npm run check:api-types, npm run typecheck, npm run build, 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 8 passed, 프롬프트 탭 포함 layout-visual-gate.spec.ts 9 passed, session-layout.spec.ts desktop/mobile 8 passed.
  • 페르소나 생성 계약 11차 + 평가 저장 packet: app/persona_generation_contract.py가 draft structured schema, prompt bundle, legacy/structured GenerateResponse payload extraction, generated draft defaults/coercion을 소유한다. 기존 routes/personas.py는 teacher/admin gate, RAG evidence 수집, engine request, provenance 조립, DB/HTTP error mapping을 계속 담당한다. SessionEvaluationWrite.from_result()/from_error()는 session evaluation 저장 packet 생성을 소유해 route의 long keyword bag을 제거했다. Node conformance runner는 StreamPacket.oneOf, payload $ref, gateway-default request sentinel과 resolved response model 분리를 읽어 event/payload/default-routing 검증 중복을 줄였고, GatewayPromptParts는 current-turn prompt split 경계를 명명했다. 검증: persona contract+review 39 passed, backend focused 120 passed, Node conformance OK, gateway contract 27 passed, npm run check:api-types, npm run typecheck.
  • M1 비언어 이벤트 4차: voice learner turn에 이미 저장하던 audio_ref/silence_ms/speech_rate/barge_in을 리뷰 API nonverbal 이벤트로 노출하고, 회기 리뷰 축어록에 침묵·발화 속도·끼어듦·음성 입력 칩을 표시했다. 2차에서는 app.turns.provider_events JSONBTurnRecord.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 다회기 케이스 아크 11차: 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.digestcase_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 색인한다. 7차에서는 submit/stream 공통 _prepare_turn_context() 경계를 만들고 DB seed recall·pinned fact가 다음 턴 EngineMessage L2/L4에 raw 이름 마스킹 상태로 주입되는 route-level 회귀를 추가했다. 8차에서는 SessionDigestInput/SessionDigestResult/SessionSummaryWrite로 종료 digest 입력·fallback 결과·DB write 인자 경계를 명시했다. 9차에서는 DigestQualityAssessment/SessionDigestWorkerOutcome로 LLM digest 후보 local quality harness를 추가해 빈/짧은 digest, raw forbidden substring, 내부 평가·CCD·상태 marker, 잘못된 S{session_no}: prefix를 fallback 유지 대상으로 판정한다. 10차에서는 session_digest_worker.pyCompressionJobGenerateRequest 변환, 주입형 engine/audit 호출, accepted-only session_summary.digest/compressed_by/token_countcase_profile.case_digest 적용 계획, one-shot DB loader를 소유한다. 11차에서는 scripts/run-session-digest-worker.py dry-run/apply runner와 SESSION_DIGEST_WORKER_ENABLED=false 기본값의 세션 종료 background scheduler 골격을 추가하고, compressed_by IS NULL loader/apply CAS로 이미 압축된 세션 재실행 race를 막는다. LLM digest worker 입력 후보는 client-visible text_masked turn과 open thread로 제한하고, 종료 상태 수치·평가 payload·CCD·raw text는 압축 prompt에 넣지 않는다. /sessions/{id}/enddigest_pending은 CompressionJob 생성 여부를 알리는 비동기 압축 필요 신호로 유지한다. 새 contradicted fact는 임의 생성하지 않고, 같은 값 재확인은 history를 늘리지 않으며, locked fact는 건드리지 않는다. learner-owned case_profile 기준 RLS insert/update를 추가했고, 관계갈등·위기·임상 추론은 자동 pinning/모순 처리에서 제외했다. 관계·임상 fact 승격 기준, 실 provider 장시간 운영, 임상 골든셋 품질평가, 재압축은 후속. 검증: py -3.11 -X utf8 -B -m pytest -p no:cacheprovider app/test_session_digest_worker.py app/test_session_memory.py -q 30 passed, py -3.11 -X utf8 -B -m pytest -p no:cacheprovider app/test_session_digest_worker.py app/test_session_memory.py app/test_session_turn_persistence.py app/test_orchestrator_masking.py app/test_runtime_policy.py -q 87 passed, py -3.11 -X utf8 -B -m py_compile app/services/session_digest_worker.py app/services/memory.py app/test_session_digest_worker.py app/test_session_memory.py, 기존 py -3.11 -X utf8 -B -m pytest -p no:cacheprovider app/test_session_memory.py -q 20 passed, 과거 py -3.11 -X utf8 -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 74 passed, npm run check:api-types, npm run typecheck.
  • C1 사례개념화 워크시트 4차: SessionReviewResponse.caseWorksheet와 리뷰 화면 카드가 탐색 11항목·호소 5영역·인지삼제/감정·보호/방해·생물심리사회 목표 초안을 축어록 근거 기반으로 제공한다. 학습자가 편집한 저장본은 PUT /sessions/{id}/review/worksheetapp.case_worksheet에 저장되고, 이후 GET /reviewsaved_by_learner 저장본을 자동 초안보다 우선 반환한다. 2026-06-28에는 data/rubrics/case-worksheet-rubric.json, schema, loader/validator service, scripts/check-case-worksheet-rubric.py를 추가해 임상팀 확정 루브릭을 외부 JSON으로 받을 scaffold를 만들었다. 현재는 status=scaffold_only, scoring_enabled=false이며 5개 section/28개 item key와 sha256 provenance만 검증한다. 64차 refactor-governance 패스에서 워크시트 템플릿 key source를 CASE_WORKSHEET_SECTION_SPECS/case_worksheet_template_item_keys()로 명시해 CLI/test가 더미 턴 없이 같은 생성 spec을 검증하게 했다. 73차에서는 기존 app.session_review_statusworksheet_status/worksheet_note/worksheet_reviewed_at을 추가하고, PUT /teacher/sessions/{id}/review-status와 교수자 SessionReview 카드에서 승인·수정요청·반려 수동 판정을 저장/표시한다. 임상팀 확정 루브릭 콘텐츠, AI 추출/채점 적용·calibration, 승인 후 잠금·재제출 정책, 재귀학습 데이터셋 approved 연계는 후속 gate다. 검증: py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_session_turn_persistence.py app/test_teacher_dashboard.py app/test_rbac_idor.py app/test_case_worksheet_rubric.py -q 40 passed, py -3.11 -X utf8 scripts/check-case-worksheet-rubric.py --json PASS, py -3.11 -X utf8 -m py_compile focused files, npm run generate:api-types, 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, 로컬 API smoke 세션 생성→저장→GET /review saved_by_learner:local smoke saved worksheet.
  • C3 이론모드 2차: theory_modeTurnContext/sessions/voice/evaluator뿐 아니라 build_turn_messages의 엔진 메시지까지 전달된다. 프론트는 persona.theory_target 기준 기본값을 유지하되, 세션 시작 전 humanistic/cbt/integrative segmented control로 학습자가 명시 선택하고 POST /sessionstheory_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 30 passed, pytest app/ -q 178 passed.
  • X2 예산 경고/저비용 평가 라우팅/evaluator cache 관측·일별 비용 추이·모델별 비용 검증 리포트 2차: ADMIN_USAGE_BUDGET_USD 설정값을 기준으로 GET /admin/usagebudget.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_reportscripts/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/마스킹 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로 합성 fixture 15케이스 평가 harness를 추가했고, data/privacy/pii-masking-eval-input.schema.jsondata/privacy/pii-masking-eval-report.schema.json으로 source/category/severity metadata와 summary-only technical_dry_run report 계약을 고정했다. 66차에서는 제 이름은 김서연입니다, 보호자 이름은 박민수입니다, 저는 최하늘입니다 자연 발화형 이름 라벨·자기소개 케이스와 이름은 중요하지 않다 negative control을 추가해 라벨 단어만 마스킹하고 실명을 남기던 구멍을 막았다. 70차에서는 guardrail.mask_pii() 내부에 선택형 한국어 PII recognizer adapter 경계를 추가했다. adapter는 import-time hard dependency가 아니며 명시 등록 전에는 비활성이고, 실패해도 기존 regex fallback이 마지막 안전망으로 유지된다. fake adapter 테스트는 regex가 못 잡는 별명/기관 span을 [NAME]/[ORG]로 마스킹하고 같은 문장의 전화번호는 후단 regex가 [PHONE]으로 처리하는지, adapter 실패 시에도 fallback이 유지되는지 검증한다. span 중복 처리는 문자별 set 할당 대신 정렬된 last_end 비교로 정리했다. 기본 CLI JSON은 masked_text/forbidden_remaining 원문 증거를 제외하며, 소속/안내/이름 NAME 오탐도 stopword로 보정했다. 로컬 dev-login 실제 /turn smoke에서 audit.llm_call_log가 8→11로 3행 증가했다(session 2460d56c-a9cb-4a40-a175-9575d510a5e9). 공개 Google OAuth 실제 /turn proof는 별도 B2 항목에 남긴다. 검증: py -3.11 -X utf8 -m py_compile apps/api/app/services/guardrail.py apps/api/app/services/pii_masking_eval.py apps/api/app/test_pii_masking_eval.py apps/api/app/test_orchestrator_masking.py scripts/evaluate-pii-masking.py, py -3.11 -X utf8 -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 47 passed, py -3.11 -X utf8 scripts/evaluate-pii-masking.py --json 15/15 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 및 관리자 사용자 관리 경로로만 부여한다. 한국어 이름/기관은 로컬 휴리스틱 마스킹, optional ko recognizer adapter 배선, 합성 fixture 평가 harness까지 완료했고, 실제 ko recognizer 모델/provider 선정, 운영 말뭉치 기반 오탐/미탐 평가, 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.
  • 공개/로컬/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.ps1vignette-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 패키징 정리 1차: API Dockerfile은 저장소 루트 컨텍스트에서 apps/apidata를 함께 복사하고, 런타임 코드의 Path(__file__).parents[...] 루트 계산은 app.paths.repo_root()로 모았다. 웹 Dockerfile은 npm lockfile 기반 npm ci/npm run build로 고정하고, web/root .dockerignore로 host node_modules와 런타임 산출물이 이미지에 섞이지 않게 했다. 구현 없는 rag.server 사이드카는 compose에서 제거하고, RAG 무거운 의존성은 INSTALL_RAG=true build arg로 API 이미지에 opt-in 설치한다. 사용자 업로드는 apiuploads 볼륨(/app/uploads)으로 분리했다. API 이미지에 로컬 apps/api/.env가 복사되어 prod fail-closed 설정을 오염시키던 보안 결함도 .dockerignore로 차단했다. API startup은 init-complete DB에서 owner 권한 DDL을 건너뛰고, non-dev에서 schema가 미완성이면 owner-run init/migration과 scripts/check-deploy-preflight.py를 요구한다. admin_engine_config 행이 없으면 env 기본 엔진 설정으로 기동한다. backend base/RAG requirements는 exact version으로 고정했고, data/kb live-coaching source pack은 commit 또는 동일 경로 provision이 필요한 런타임 입력으로 문서화했다. 검증: docker compose -f infra/docker-compose.yml config --quiet 더미 필수 env 통과, API/Web image build 통과, API image .env* 미포함, prod import smoke 통과, fresh docker compose up -d --build smoke 통과(/api/health 200, db:true, engine:true, api healthy), python scripts/check-deploy-preflight.py --skip-db --env-file infra\.env.example --allow-placeholder-secrets 통과. 실제 이식 전에는 배포지 infra/.envAPP_DB_PASSWORD, OAuth client id/secret, OPENAI_API_KEY, SESSION_SECRET 등 소유자 비밀값을 채워야 한다.