검증 증거 문서 보강

This commit is contained in:
Yun Chan 2026-07-01 12:48:26 +09:00
parent 69fece56bf
commit 7c41c3ce79
5 changed files with 57 additions and 18 deletions

View file

@ -61,6 +61,14 @@ interface LiveCoachEvent {
suggestion: {
title: string;
message: string;
sources?: Array<{
source_id: string;
title: string;
locator?: string | null;
kb_kind?: string | null;
version?: string | null;
citation?: string | null;
}>;
};
}
@ -619,30 +627,59 @@ test.describe("session persistence", () => {
const coachResponse = await coachResponsePromise;
await expectResponseOk(coachResponse);
const coachSuggestion = (await coachResponse.json()) as LiveCoachEvent["suggestion"];
const workbookSource = coachSuggestion.sources?.find(
(source) => source.source_id === "workbook_0615_case_conceptualization",
);
expect(workbookSource, "live coach response should include the licensed workbook source pack").toBeTruthy();
expect(workbookSource?.version).toBe("2026-06-15");
expect(workbookSource?.citation ?? "").toContain("0615");
await expect(page.locator(".sx-coach-card").getByText(coachSuggestion.title)).toBeVisible({
timeout: 30_000,
});
await page.locator(".sx-coach-card").getByRole("button", { name: "근거 보기" }).click();
const evidenceDialog = page.locator(".sx-coach-modal [role='dialog']");
await expect(evidenceDialog).toBeVisible({ timeout: 15_000 });
await expect(evidenceDialog).toContainText(workbookSource!.title);
await expect(evidenceDialog).toContainText("2026-06-15");
await expect(evidenceDialog).toContainText("0615");
await evidenceDialog.getByRole("button", { name: "닫기" }).click();
await expect(evidenceDialog).toHaveCount(0);
const historyResponse = await page.request.get(`/api/sessions/${sessionId}/live-coach`);
await expectResponseOk(historyResponse);
const history = (await historyResponse.json()) as LiveCoachHistoryResponse;
expect(history.source).toBe("database");
expect(
history.events.some(
const persistedCoachEvent = history.events.find(
(event) =>
event.turn_seq === 1 &&
event.learner_text_excerpt?.includes("가장 버거운 감정") &&
event.suggestion.title === coachSuggestion.title,
);
expect(persistedCoachEvent, "DB-backed live coach history should include the delivered coaching event").toBeTruthy();
expect(
persistedCoachEvent?.suggestion.sources?.some(
(source) =>
source.source_id === workbookSource!.source_id &&
source.version === workbookSource!.version &&
(source.citation ?? "").includes("0615"),
),
"DB-backed live coach history should preserve source pack metadata",
).toBe(true);
await page.goto(`/learn/session/${sessionId}`);
await expect(page.locator(".sx-utt.is-learner").filter({ hasText: learnerText })).toBeVisible({
timeout: 15_000,
});
await expect(page.locator(".sx-utt.is-learner .sx-utt__coach-mark")).toBeVisible({
const coachMark = page.locator(".sx-utt.is-learner .sx-utt__coach-mark");
await expect(coachMark).toBeVisible({
timeout: 15_000,
});
await coachMark.click();
const historyDialog = page.locator(".sx-coach-history [role='dialog']");
await expect(historyDialog).toBeVisible({ timeout: 15_000 });
await expect(historyDialog).toContainText(workbookSource!.title);
await expect(historyDialog).toContainText("2026-06-15");
await expect(historyDialog).toContainText("0615");
});
test("persists voice nonverbal metadata into DB-backed review @single-run", async ({ page }) => {

View file

@ -547,7 +547,7 @@
<p class="dg-note">112차 적용(2026-07-01): 교수자 회기 리뷰 데스크톱에서 <code>overview</code>/<code>transcript</code>가 여러 CSS Grid row를 가로질러 오른쪽 검토 rail 높이를 나눠 먹으며 중앙에 큰 빈 row가 생기던 문제를 제거했다. 1181px 이상 교수자 리뷰는 main column + review rail 2열 wrapper로 분리하고, main 내부는 요약 전체폭, 차트/회기 흐름 2열, 축어록 전체폭으로 배치해 서로 다른 rail 높이가 빈공간을 만들지 않게 했다. 회귀 방지로 <code>layout-visual-gate.spec.ts</code>에 교수자 리뷰 폭별 dead vertical gap 검증을 추가했다. 검증: <code>npm run typecheck</code>, <code>npm run build</code>, <code>session-review</code> desktop 8 passed, <code>layout-visual-gate</code> 10 passed, <code>session-layout</code> desktop/mobile 8 passed.</p>
<p class="dg-note">113차 적용(2026-07-01): 학습자 <code>/learn/practice</code> 페르소나 리스트가 긴 한글 주호소 요약에서 카드 내부를 클립하던 문제를 수정했다. <code>.lh-list-pane</code>의 viewport 고정 높이와 내부 스크롤을 풀고, <code>.lh-personas</code>/<code>.lh-persona</code>가 콘텐츠 높이를 그대로 만들게 했으며, <code>.lh-persona__summary</code> 3줄 clamp를 제거했다. <code>learner.spec.ts</code>에는 각 페르소나 카드의 summary/body/card scroll overflow가 0인지 재는 regression guard를 추가했다. 검증: <code>npm run typecheck</code>, <code>npm run build</code>, 브라우저 rect smoke 1095px/1440px에서 카드 overflow 0 및 문서 가로 overflow 0. 현재 로컬 <code>127.0.0.1:8000/health</code>가 timeout이라 real API 기반 전체 <code>learner.spec.ts</code>는 이번 패스에서 미실행.</p>
<p class="dg-note">114차 적용(2026-07-01): 회기 종료 후 background deep 평가가 실제 durable DB row로 저장되는지 fixture 없이 검증했다. <code>_schedule_session_evaluation()</code>은 생성한 task를 반환하고 done callback에서 crash/cancel을 로그로 관찰하며, 평가 결과·timeout/error 저장이 durable store에 닿지 않으면 명시 error 로그를 남긴다. 교수자 명시 재평가 API도 평가 산출물이 생성됐지만 저장 실패하면 503으로 표면화한다. <code>GET /eval/sessions/{id}/evaluation</code><code>status/error/durable</code>을 노출해 캐시 착시와 DB 저장 증거를 구분한다. 새 DB-backed E2E는 학습자 세션 생성→턴→종료→teacher 로그인→<code>/eval/.../evaluation</code> polling→교수자 리뷰 UI <code>평가 완료</code>까지 실제 API/DB/엔진으로 확인한다. 검증: API 전체 309 passed, <code>npm run check:api-types</code>, <code>npm run typecheck</code>, <code>PLAYWRIGHT_PORT=5205 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1</code> 7 passed, <code>PLAYWRIGHT_PORT=5203 npx playwright test e2e/session-review.spec.ts --project=chromium-desktop --project=chromium-mobile --workers=1</code> 16 passed, 현재 Playwright 수집 기준 <code>162 tests in 18 files</code>.</p>
<p class="dg-note">115차 적용(2026-07-01): 교수자 학생 분석을 <code>/teach</code> 대시보드에서 분리해 좌측 메뉴의 <code>/teach/analysis</code> 전용 작업면으로 이동했다. 교수 콘솔은 검토 큐·위기 알림·페르소나 검수·최근 회기 triage만 맡고, 학생 분석은 학습자 목록 레일, 사용자별 추이, 단계 분포, 전체 회기 타임라인, 회기 리뷰 드릴다운 구조를 맡는다. 회귀 방지로 <code>teacher.spec.ts</code>는 콘솔에 분석 패널이 없고 메뉴를 통해 학생 분석 페이지로 이동하는지 확인하며, <code>layout-visual-gate.spec.ts</code>는 학생 분석 페이지를 7개 폭에서 별도 캡처·검사한다. 검증: <code>npm run typecheck</code>, <code>npm run build</code>, <code>npm run check:api-types</code>, <code>PLAYWRIGHT_PORT=5211 npx playwright test e2e/teacher.spec.ts --project=chromium-single-run --workers=1</code> 9 passed, <code>PLAYWRIGHT_PORT=5214 npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run --workers=1</code> 11 passed, SSOT checker PASS, 현재 Playwright 수집 기준 163 tests / 18 files.</p>
<p class="dg-note">115차 적용(2026-07-01): 교수자 학생 분석을 <code>/teach</code> 대시보드에서 분리해 좌측 메뉴의 <code>/teach/analysis</code> 전용 작업면으로 이동했다. 교수 콘솔은 검토 큐·위기 알림·페르소나 검수·최근 회기 triage만 맡고, 학생 분석은 전체 학습자 검색 테이블을 먼저 보여준다. 각 학습자는 한 줄 행으로 표시하고, 우측 펼침 버튼은 행 아래에 미니 추이·기법 태그·최근 기록을 확장한다. 학습자 이름 또는 상세 보기 버튼은 <code>?learner=</code> 상세 드릴다운으로 이동하며, 상세 화면은 추이·전체 회기·단계 분석 탭으로 선택 학습자의 전체 회기 이력을 나눈다. 회귀 방지로 <code>teacher.spec.ts</code>는 콘솔에 분석 패널이 없고 메뉴를 통해 학생 분석 페이지로 이동한 뒤 검색·행 펼침·상세 진입·전체 회기 탭을 확인하며, <code>layout-visual-gate.spec.ts</code>는 학생 분석 목록/상세 화면을 7개 폭에서 별도 캡처·검사한다. 검증: <code>npm run typecheck</code>, <code>npm run build</code>, <code>npm run check:api-types</code>, <code>PLAYWRIGHT_PORT=5215 npx playwright test e2e/teacher.spec.ts --project=chromium-single-run --workers=1</code> 9 passed, <code>PLAYWRIGHT_PORT=5217 npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run --workers=1</code> 12 passed, 현재 Playwright 수집 기준 166 tests / 18 files.</p>
<p class="dg-note">116차 적용(2026-07-01): 원천문서 갭 E2E 목표에 맞춰 C1/C2 silent failure를 추가로 닫았다. C1은 학습자가 저장한 사례개념화 워크시트가 <code>app.case_worksheet</code>를 거쳐 교수자 검토 화면 read-only 값으로 복원되고, 교수자 <code>수정요청</code> 검수 메모가 <code>teacherReview.worksheetStatus</code>/<code>worksheetNote</code>로 재조회되는지 실제 브라우저/API/DB E2E로 고정했다. C2는 <code>app.safety_events</code>가 runtime bootstrap/healthcheck에서 빠져 DB 정상처럼 보일 수 있던 구멍과, <code>jsonb</code> codec 위에 이미 직렬화한 문자열을 넘겨 detail이 JSON string으로 저장되던 구멍을 수정했다. <code>record_safety_event()</code>는 RLS용 <code>ai_context=true</code>로 저장하고, alert list 실패는 non-dev에서 fail-closed한다. 검증: <code>python -X utf8 -m pytest -p no:cacheprovider app/test_session_turn_persistence.py app/test_teacher_dashboard.py app/test_runtime_policy.py -q</code> 59 passed, <code>PLAYWRIGHT_PORT=5228 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1 --grep "case worksheet|crisis safety"</code> 2 passed, 현재 Playwright 수집 기준 <code>166 tests in 18 files</code>.</p>
<p class="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>
<p class="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>
@ -670,8 +670,8 @@
<div class="scard-body"><div class="kv"><b>현재</b><p>PDF(9·16·18쪽): 자해·자살 감지 시 <b>대화 중단 + 자살예방상담 109 안내 + 지도교수 자동 알림</b>, 안전은 <b>LLM 밖 별도 CrisisGate</b>. 구현: <code>CRISIS_HOTLINE_NUMBER=109</code>, <code>TurnResponse.crisis_resource</code>/<code>conversation_stopped</code>, REST·SSE·voice 경로의 엔진 호출 전 중단, <code>app.safety_events</code> detail 적재, 교수자 대시보드 안전 알림 큐.</p></div><div class="kv"><b>검증</b><p><code>pytest app/test_session_turn_persistence.py app/test_voice_ws.py -q</code> 20 passed, 안전 이벤트 DB insert payload 테스트 포함. <code>pytest app/test_teacher_dashboard.py app/test_session_turn_persistence.py app/test_voice_ws.py -q</code> 21 passed.</p></div><div class="kv k-warn"><b>잔여</b><p>비밀보장 예외고지·생명유지서약 등 임상 문안, 실시간 push/메일 알림, 지역 자원 확장, 운영 정책 문구는 소유자·임상팀 확정 필요. 현재 교수자 알림은 <code>safety_events</code> 기반 대시보드 큐다.</p></div></div>
</article>
<article class="scard" data-status="done" data-cat="교수자·평가" data-owner="0">
<button class="scard-head" aria-expanded="false"><span class="chip c-done">CRITICAL UX 1차 DONE</span><span class="scard-mid"><span class="scard-title">교수자 '학습자 이력·성장 추적' UI/UX<span class="critbadge">핵심 가치</span></span><span class="scard-sum">별도 학생 분석 메뉴에서 학습자별 회기 이력, 적절성 추이, 라포, 기법 태그, 전체 회기 타임라인을 확인.</span></span><span class="caret" aria-hidden="true"></span></button>
<div class="scard-body"><div class="kv"><b>현재</b><p><code>TeacherDashboardResponse.learner_growth</code><code>GET /teacher/learners/{learner_id}/analysis</code>가 턴별 fast-loop 평가의 <code>appropriateness</code>, <code>rapport_signal</code>, 기법 태그를 학습자별로 집계한다. <code>/teach</code>는 검토 큐·위기 알림·페르소나 검수·최근 회기 triage만 맡고, <code>/teach/analysis</code>학습자 목록 레일, 회기별 막대 추이, 평균 라포, 변화폭, 단계 분포, 전체 회기 타임라인을 표시한다. 평가가 없는 회기는 "평가 부족"으로 남겨 가짜 곡선을 만들지 않는다. 회기 행은 <code>/teach/session/{id}/review</code>로 진입해 같은 회기 리뷰를 교수자 읽기 전용으로 고, 워크시트는 학습자 제출물 수정 없이 검토 전용으로 표시한다.</p></div><div class="kv"><b>검증</b><p><code>pytest app/test_teacher_dashboard.py app/test_session_turn_persistence.py app/test_voice_ws.py -q</code> 21 passed, <code>pytest app/test_rbac_idor.py app/test_teacher_dashboard.py -q</code> 9 passed, <code>npm run check:api-types</code>, <code>npm run typecheck</code>, <code>npm run build</code> passed. <code>npx playwright test e2e/teacher.spec.ts --project=chromium-single-run --workers=1</code> 4 passed.</p></div><div class="kv k-warn"><b>잔여</b><p>자기효능감·기술숙련도·수련만족도 사전사후(H1)와 교수자 코멘트/검수 워크플로 확장은 별도 평가설계·임상팀 문항 확정이 필요하다.</p></div></div>
<button class="scard-head" aria-expanded="false"><span class="chip c-done">CRITICAL UX 1차 DONE</span><span class="scard-mid"><span class="scard-title">교수자 '학습자 이력·성장 추적' UI/UX<span class="critbadge">핵심 가치</span></span><span class="scard-sum">학생 분석 메뉴에서 전체 학습자 검색 테이블, 행 펼침 요약, 학습자별 상세 탭과 전체 회기 타임라인을 확인.</span></span><span class="caret" aria-hidden="true"></span></button>
<div class="scard-body"><div class="kv"><b>현재</b><p><code>TeacherDashboardResponse.learner_growth</code><code>GET /teacher/learners/{learner_id}/analysis</code>가 턴별 fast-loop 평가의 <code>appropriateness</code>, <code>rapport_signal</code>, 기법 태그를 학습자별로 집계한다. <code>/teach</code>는 검토 큐·위기 알림·페르소나 검수·최근 회기 triage만 맡고, <code>/teach/analysis</code>검색 가능한 1행 학습자 테이블을 먼저 보여준다. 우측 펼침 버튼은 행 아래에 회기별 미니 추이, 평균 라포, 변화폭, 기법 태그, 최근 기록을 확장하고, 이름 또는 상세 보기 버튼은 <code>?learner=</code> 상세 화면으로 들어간다. 상세 화면은 추이·전체 회기·단계 분석 탭으로 나뉘며, 회기 행은 <code>/teach/session/{id}/review</code>로 진입해 같은 회기 리뷰를 교수자 읽기 전용으로 연다. 평가가 없는 회기는 "평가 부족"으로 남겨 가짜 곡선을 만들지 않고, 워크시트는 학습자 제출물 수정 없이 검토 전용으로 표시한다.</p></div><div class="kv"><b>검증</b><p><code>python -X utf8 -m pytest -p no:cacheprovider app/test_teacher_dashboard.py -q</code> 6 passed, <code>npm run check:api-types</code>, <code>npm run typecheck</code>, <code>npm run build</code> passed. <code>PLAYWRIGHT_PORT=5215 npx playwright test e2e/teacher.spec.ts --project=chromium-single-run --workers=1</code> 9 passed, <code>PLAYWRIGHT_PORT=5217 npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run --workers=1</code> 12 passed.</p></div><div class="kv k-warn"><b>잔여</b><p>자기효능감·기술숙련도·수련만족도 사전사후(H1)와 교수자 코멘트/검수 워크플로 확장은 별도 평가설계·임상팀 문항 확정이 필요하다.</p></div></div>
</article>
<article class="scard" data-status="done" data-cat="교수자·평가" data-owner="0">
<button class="scard-head" aria-expanded="false"><span class="chip c-done">검토 상태 1차 DONE</span><span class="scard-mid"><span class="scard-title">교수자 회기 검토 상태 저장</span><span class="scard-sum"><code>app.session_review_status</code><code>PUT /teacher/sessions/{session_id}/review-status</code>로 검토 메모·완료 상태를 저장.</span></span><span class="caret" aria-hidden="true"></span></button>
@ -936,7 +936,7 @@
<tr><td>M3 auth claim mapping</td><td><code>python -B -m pytest -p no:cacheprovider app/test_auth_providers.py -q</code></td><td>30 passed; Google/SAML cohort maps, provider external_id, provider error reason, PKCE/state, HttpOnly cookie, SAML fixture ACS covered</td></tr>
<tr><td>Auth managed-user upsert boundary</td><td><code>py_compile</code> / <code>py -3.11 -X utf8 -B -m pytest -p no:cacheprovider app/test_auth_providers.py app/test_admin_ops.py app/test_rbac_idor.py app/test_runtime_policy.py app/test_session_turn_persistence.py app/test_learner_dashboard.py app/test_teacher_dashboard.py -q</code> / <code>npm run check:api-types</code></td><td><code>ManagedUserUpsertInput</code> owns managed-user create/reactivate inputs; <code>create_session()</code>, admin user create, and direct auth regression calls no longer pass long keyword bags. <code>ManagedUserMemoryInput</code> remains DB→memory fallback/store sync, and <code>ManagedUserPatch</code> remains partial profile/admin update. Focused backend 103 passed; API type drift check passed.</td></tr>
<tr><td>CrisisGate 109</td><td><code>python -m pytest app/test_session_turn_persistence.py app/test_voice_ws.py -q</code> / <code>PLAYWRIGHT_PORT=5228 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1 --grep "crisis safety"</code></td><td>real crisis stops before engine, returns 109 resource, and DB insert uses <code>ai_context=true</code>. <code>db.healthcheck()</code> now requires <code>app.safety_events</code>; alert detail handles legacy double-encoded JSON string rows; full browser+API+DB E2E verifies <code>safety_events</code> reaches the DB-backed teacher safety queue with resource 109.</td></tr>
<tr><td>Professor review workflow</td><td><code>python -X utf8 -m pytest -p no:cacheprovider app/test_teacher_dashboard.py -q</code> / <code>PLAYWRIGHT_PORT=5211 npx playwright test e2e/teacher.spec.ts --project=chromium-single-run --workers=1</code> / <code>PLAYWRIGHT_PORT=5214 npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run --workers=1</code> / <code>npm run check:api-types</code> / <code>npm run typecheck</code> / <code>npm run build</code></td><td>5 backend passed + 9 E2E passed + strict visual gate 11 passed; <code>/teach</code> now keeps only triage queues, and <code>/teach/analysis</code> returns a selected learner's full session timeline, unrestricted trend points, stage breakdown, and review counts in a separate student analysis page.</td></tr>
<tr><td>Professor review workflow</td><td><code>python -X utf8 -m pytest -p no:cacheprovider app/test_teacher_dashboard.py -q</code> / <code>PLAYWRIGHT_PORT=5215 npx playwright test e2e/teacher.spec.ts --project=chromium-single-run --workers=1</code> / <code>PLAYWRIGHT_PORT=5217 npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run --workers=1</code> / <code>npm run check:api-types</code> / <code>npm run typecheck</code> / <code>npm run build</code></td><td>6 backend passed + 9 E2E passed + strict visual gate 12 passed; <code>/teach</code> now keeps only triage queues, and <code>/teach/analysis</code> starts with a searchable one-row-per-learner table, right-side row expansion, and <code>?learner=</code> detail tabs for full session timeline, unrestricted trend points, stage breakdown, and review counts.</td></tr>
<tr><td>Learner/teacher growth metrics</td><td><code>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</code></td><td>59 backend passed; shared <code>session_metrics</code> now prefers rehydrated <code>label_ko</code> for technique labels while preserving <code>label/name/id/code</code> fallback. <code>alternative_utterances</code> string and <code>text/suggestion</code> dict inputs are pinned before any future evaluator adapter extraction.</td></tr>
<tr><td>Teacher review status</td><td><code>pytest app/test_teacher_dashboard.py app/test_rbac_idor.py app/test_learner_dashboard.py -q</code> / <code>npm run typecheck</code> / <code>npm run build</code> / <code>teacher.spec.ts</code></td><td>12 backend passed + 4 E2E passed; <code>app.session_review_status</code>, teacher dashboard <code>review_status/review_note/reviewed_at</code>, worksheet <code>worksheet_status/worksheet_note/worksheet_reviewed_at</code>, and <code>PUT /teacher/sessions/{session_id}/review-status</code> are documented. 검토 큐 visible 회귀와 교수자 전용 review grid 영역도 테스트로 고정했다.</td></tr>
<tr><td>Persona authoring + generation contract</td><td><code>py -3.11 -X utf8 -B -m pytest -p no:cacheprovider app/test_persona_generation_contract.py app/test_persona_review.py -q</code></td><td>39 passed; teacher/admin draft create/read/update/review submit, approved persona revision clone, code-family archive delete, learner blocked, approved-only catalog maintained. <code>app/persona_generation_contract.py</code> owns draft structured schema, prompt bundle id/version/hash, legacy <code>GenerateResponse</code> payload extraction, and generated draft defaults/coercion.</td></tr>
@ -966,7 +966,7 @@
<tr><td>Browser SSE stream + AI tutor persistence</td><td><code>PLAYWRIGHT_PORT=5205 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1</code></td><td>7 passed; 기존 direct <code>/turn</code> real API 저장 검증에 더해 실제 Session UI가 <code>openSessionStream()</code>으로 <code>POST /sessions/{id}/stream</code>을 호출하고, stream 완료 후 DB-backed <code>/review</code>가 learner/client 축어록을 반환하는지 확인한다. 추가로 코칭 모드에서 <code>POST /live-coach</code> 성공 뒤 DB-backed <code>GET /live-coach</code> 이력과 재로딩 후 학습자 발화의 <code>C</code> 마커가 유지되는지 확인하고, voice WS <code>stt_result</code> 및 실제 Session 마이크 UI 생성 비언어 메타가 DB-backed review <code>nonverbal</code>로 파생되는지도 고정한다. 같은 spec은 Phase 3 pre/post 저장과 세션 종료 background deep 평가의 durable DB row까지 검증한다. <code>db</code>/<code>engine</code> health가 false면 skip하고, stream/live-coach/evaluation 503/error는 실패로 취급한다.</td></tr>
<tr><td>C3 theory-mode reevaluation</td><td><code>C:\Users\encep\AppData\Local\Programs\Python\Python311\python.exe -X utf8 -m pytest -p no:cacheprovider app/test_eval_routes.py -q</code> / <code>app/test_eval_routes.py app/test_notifications.py app/test_session_turn_persistence.py</code></td><td>eval route 5 passed, focused backend 39 passed. 수동 session 재평가는 persona 기본 <code>theory_target</code>보다 학습자가 선택한 session <code>theory_mode</code>를 우선하고, turn 재평가도 <code>TurnContext.theory_mode</code>에 같은 값을 전달한다.</td></tr>
<tr><td>Crisis safety gate UI</td><td><code>PLAYWRIGHT_PORT=5193 npx playwright test e2e/session-mvp.spec.ts --project=chromium-single-run --workers=1</code></td><td>2 passed; mock SSE <code>safety</code> + <code>done.conversation_stopped</code>에서 안전 자원 109와 입력 disabled가 유지되고, 빈 client reply를 <code>내담자 응답 없음</code>으로 덮지 않으며 live-coach를 호출하지 않는지 검증한다.</td></tr>
<tr><td>Persona/session review UI E2E</td><td><code>PLAYWRIGHT_PORT=5211 npx playwright test e2e/teacher.spec.ts --project=chromium-single-run --workers=1</code></td><td>9 passed; teacher approves pending persona, opens pending/recent review detail, and reaches the separate student analysis page from the menu before switching full learner timelines.</td></tr>
<tr><td>Persona/session review UI E2E</td><td><code>PLAYWRIGHT_PORT=5215 npx playwright test e2e/teacher.spec.ts --project=chromium-single-run --workers=1</code></td><td>9 passed; teacher approves pending persona, opens pending/recent review detail, reaches the separate student analysis page from the menu, searches the learner table, expands a learner row, and drills into the full learner timeline tab.</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 s2s decision memo</td><td><code>docs/decisions/voice-s2s-poc.md</code></td><td>criteria recorded; keep/drop decision remains owner DECIDE</td></tr>
<tr><td>Hanshin data/SSO gate</td><td><code>docs/ops/hanshin-data-governance-gate.md</code></td><td>artifact created; written external evidence still required</td></tr>
@ -1003,10 +1003,10 @@
<tr><td>Voice/session focused</td><td><code>voice + voice-success + session-layout</code></td><td>15 passed</td></tr>
<tr><td>Layout redesign handoff</td><td><code>docs/ops/layout-redesign-handoff-2026-06-26.md</code></td><td>subagent scopes, files, verification, remaining visual review recorded</td></tr>
<tr><td>Layout redesign focused E2E</td><td><code>npx playwright test e2e/learner.spec.ts e2e/session-layout.spec.ts e2e/session-review.spec.ts e2e/admin.spec.ts e2e/settings.spec.ts e2e/teacher.spec.ts --project=chromium-desktop --project=chromium-mobile</code></td><td>54 passed after isolated Admin tablet flake rerun</td></tr>
<tr><td>Strict layout visual gate</td><td><code>PLAYWRIGHT_PORT=5214 npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run --workers=1</code></td><td>11 passed; 11 screens × 7 widths(390/720/861/900/1024/1280/1440), horizontal overflow 0 + control clip 0, 77 full-page screenshots. 학생 분석 전용 화면과 빈 회기리뷰 fixture 포함.</td></tr>
<tr><td>Strict layout visual gate</td><td><code>PLAYWRIGHT_PORT=5217 npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run --workers=1</code></td><td>12 passed; 12 screens × 7 widths(390/720/861/900/1024/1280/1440), horizontal overflow 0 + control clip 0, 84 full-page screenshots. 학생 분석 목록 테이블/상세 화면과 빈 회기리뷰 fixture 포함.</td></tr>
<tr><td>Parallel visual quality review</td><td><code>workflow layout-visual-quality-review (7 agents)</code></td><td>49 screenshots read; per-screen findings(critical 1, major 8+, minor) → fix verdicts</td></tr>
<tr><td>Parallel layout fixes</td><td><code>6 agent team: learner-home/session/review/professor/admin/settings (disjoint files)</code></td><td>all fixes applied; <code>npm run typecheck</code> OK; no cross-file conflicts</td></tr>
<tr><td>Strict gate re-run (post-fix)</td><td><code>PLAYWRIGHT_PORT=5214 npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run --workers=1</code></td><td>11 passed; re-screenshotted 77</td></tr>
<tr><td>Strict gate re-run (post-fix)</td><td><code>PLAYWRIGHT_PORT=5217 npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run --workers=1</code></td><td>12 passed; re-screenshotted 84</td></tr>
<tr><td>Layout focused E2E re-run (post-fix)</td><td><code>learner/session-layout/session-review/admin/settings/teacher × desktop+mobile</code></td><td>54 passed; no regression</td></tr>
<tr><td>Adversarial visual verify</td><td><code>workflow layout-visual-verify (7 agents)</code></td><td>7/7 accept; critical/major resolved, regression 0; minor polish backlog recorded</td></tr>
<tr><td>Engine config 운영값</td><td><code>GET /admin/engine-config</code></td><td>claude_cli / http://127.0.0.1:9099 / gateway-default, durable, source=database (의도값 일치)</td></tr>

View file

@ -381,8 +381,10 @@ session lifecycle을 유지하며, future Node read API는 이 read-model contra
- `PUT /teacher/sessions/{session_id}/review-status` — 교수자/관리자가 회기 검토 메모와 상태를 저장한다.
저장 대상은 `app.session_review_status`이며, 검토 완료된 회기는 pending queue에서 제외된다.
- `/teach` 교수 콘솔은 검토 큐·위기 알림·페르소나 검수·최근 회기 triage만 맡는다.
`/teach/analysis` 학생 분석은 별도 메뉴/라우트로 분리되어 담당 학습자 목록, 선택 학습자의
전체 회기·단계 분포·회기별 추이를 보여준다. `/teach/session/:sessionId/review` 화면은 같은
`/teach/analysis` 학생 분석은 별도 메뉴/라우트로 분리되어 전체 담당 학습자를 검색 가능한
1행 테이블로 먼저 보여주고, 우측 펼침으로 행 아래 미니 추이·기법·최근 기록을 확장한다.
학습자 이름 또는 상세 보기 버튼은 `?learner=` 상세 드릴다운으로 이동하며, 상세 화면은
추이·전체 회기·단계 분석 탭으로 선택 학습자의 전체 이력을 보여준다. `/teach/session/:sessionId/review` 화면은 같은
`GET /sessions/{id}/review` 자료를 교수자 읽기 전용으로 표시하고, 검토 메모 저장은 위 teacher endpoint로 분리한다.
### 2.9.2 운영 메일 알림 — `app/services/notifications.py`

View file

@ -268,17 +268,17 @@ VITE_API_BASE=http://127.0.0.1:8000 npm run e2e # 프록시 대신 API
- 2026-07-01 focused 검증: `PLAYWRIGHT_PORT=5205 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1` **7 passed**. 실제 브라우저 `openSessionStream()``/sessions/{id}/stream` → DB-backed `/review` 축어록 저장 경로, AI 튜터 코칭 이력 저장/재로딩, WebSocket `stt_result` 음성 비언어 메타데이터, Session 마이크 UI가 생성한 voice activity/silence 메타, Phase 3 pre/post 점수의 DB-backed 저장/재조회, 그리고 세션 종료 background deep 평가가 durable DB row로 저장되어 교수자 리뷰가 `평가 완료`로 전환되는지 검증한다.
- 2026-07-01 focused 검증: `PLAYWRIGHT_PORT=5228 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1 --grep "case worksheet|crisis safety"` **2 passed**. C1 학습자 워크시트 저장→교수자 수정요청 검수와 C2 위기 신호→`app.safety_events`→DB-backed 교수자 안전 알림 큐를 실제 브라우저/API/DB로 검증한다.
- 2026-07-01 focused 검증: `PLAYWRIGHT_PORT=5193 npx playwright test e2e/session-mvp.spec.ts --project=chromium-single-run --workers=1` **2 passed**. MVP 종료/리뷰 흐름과 위기 안전 게이트가 빈 내담자 응답 신호에 덮이지 않는지 검증한다.
- 2026-07-01 focused 검증: `PLAYWRIGHT_PORT=5211 npx playwright test e2e/teacher.spec.ts --project=chromium-single-run --workers=1` **9 passed**. 교수자 콘솔의 페르소나 검수, 검토 큐, 최근 회기 진입과 별도 `/teach/analysis` 학생 분석 메뉴, 전체 회기 타임라인 전환을 검증한다.
- 2026-07-01 focused 검증: `PLAYWRIGHT_PORT=5215 npx playwright test e2e/teacher.spec.ts --project=chromium-single-run --workers=1` **9 passed**. 교수자 콘솔의 페르소나 검수, 검토 큐, 최근 회기 진입과 별도 `/teach/analysis` 학생 분석 메뉴, 학습자 검색 테이블, 행 펼침, 상세 드릴다운, 전체 회기 탭 전환을 검증한다.
레이아웃·시각 회귀 게이트(핵심 합격선):
| 게이트 | 스펙 | 구성 | 개수 |
|---|---|---|---|
| 세션 레이아웃 | `e2e/session-layout.spec.ts` | 4 테스트 × (desktop+mobile) | **8 / 8** |
| 시각 레이아웃 게이트 | `e2e/layout-visual-gate.spec.ts` | `@single-run`, 11개 화면 × 7개 폭 검사 + 다크 테마 assertion | **11 / 11** |
| 시각 레이아웃 게이트 | `e2e/layout-visual-gate.spec.ts` | `@single-run`, 12개 화면 × 7개 폭 검사 + 다크 테마 assertion | **12 / 12** |
| 레이아웃 포커스(재설계 화면) | `session-layout`·`session-review`·`admin`·`learner`·`settings`·`teacher`, `@single-run` 제외 | desktop+mobile 병렬 | **54** |
> `layout-visual-gate`는 7개 폭(390/720/861/900/1024/1280/1440)에서 11개 핵심 화면의 가로
> `layout-visual-gate`는 7개 폭(390/720/861/900/1024/1280/1440)에서 12개 핵심 화면의 가로
> 오버플로·잘린 컨트롤·다크 테마 적용을 검사하고 전체 페이지 스크린샷을
> `node_modules/.tmp/layout-gate/`에 남긴다.
> `session-layout`은 회기 전/활성 화면이 뷰포트를 벗어나지 않는지, 우측 패널이 코어 영역을

View file

@ -24,7 +24,7 @@
최신 동기화 추가(2026-07-01): 교수자 회기 리뷰 데스크톱에서 `overview`/`transcript`가 여러 CSS Grid row를 가로질러 오른쪽 검토 rail 높이를 나눠 먹으며 중앙에 큰 빈 row가 생기던 문제를 제거했다. 1181px 이상 교수자 리뷰는 main column + review rail 2열 wrapper로 분리하고, main 내부는 요약 전체폭, 차트/회기 흐름 2열, 축어록 전체폭으로 배치한다. 회귀 방지로 `layout-visual-gate.spec.ts`에 교수자 리뷰 폭별 dead vertical gap 검증을 추가했다. 검증: `npm run typecheck`, `npm run build`, `npx playwright test e2e/session-review.spec.ts --project=chromium-desktop --workers=1` 8 passed, `npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run --workers=1` 10 passed, `npx playwright test e2e/session-layout.spec.ts --project=chromium-desktop --project=chromium-mobile --workers=1` 8 passed.
최신 동기화 추가(2026-07-01): 교수자 학생 분석을 콘솔에서 분리했다. `GET /teacher/learners/{learner_id}/analysis`는 담당 범위 안의 특정 학습자 전체 회기를 오래된 순서로 반환하고, 제한 없는 회기별 추이 point, 라포·탐색·개입·정리 단계 분포, 검토 대기/완료 카운트를 함께 내려준다. `/teach`는 검토 큐·위기 알림·페르소나 검수·최근 회기 triage만 맡고, 좌측 메뉴의 `/teach/analysis`가 학습자 목록 레일 → 사용자별 분석 → 전체 회기 타임라인 → 회기 리뷰 드릴다운을 맡는다. 검증: `python -X utf8 -m pytest -p no:cacheprovider app/test_teacher_dashboard.py -q` 5 passed, `npm run check:api-types`, `npm run typecheck`, `npm run build`, `PLAYWRIGHT_PORT=5211 npx playwright test e2e/teacher.spec.ts --project=chromium-single-run --workers=1` 9 passed, `PLAYWRIGHT_PORT=5214 npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run --workers=1` 11 passed, `python -X utf8 scripts/check-dev-dashboard-ssot.py --json` PASS, `npx playwright test --list` 현재 163 tests / 18 files.
최신 동기화 추가(2026-07-01): 교수자 학생 분석을 콘솔에서 분리했다. `GET /teacher/learners/{learner_id}/analysis`는 담당 범위 안의 특정 학습자 전체 회기를 오래된 순서로 반환하고, 제한 없는 회기별 추이 point, 라포·탐색·개입·정리 단계 분포, 검토 대기/완료 카운트를 함께 내려준다. `/teach`는 검토 큐·위기 알림·페르소나 검수·최근 회기 triage만 맡고, 좌측 메뉴의 `/teach/analysis`는 전체 학습자 검색 테이블 → 우측 행 펼침 요약 → `?learner=` 사용자별 상세 → 추이/전체 회기/단계 분석 탭 → 회기 리뷰 드릴다운을 맡는다. 검증: `python -X utf8 -m pytest -p no:cacheprovider app/test_teacher_dashboard.py -q` 6 passed, `npm run check:api-types`, `npm run typecheck`, `npm run build`, `PLAYWRIGHT_PORT=5215 npx playwright test e2e/teacher.spec.ts --project=chromium-single-run --workers=1` 9 passed, `PLAYWRIGHT_PORT=5217 npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run --workers=1` 12 passed, `npx playwright test --list` 현재 166 tests / 18 files.
최신 동기화 추가(2026-07-01): 원천문서 갭 E2E 탐색을 이어서 H1/C3 silent failure 후보를 좁혔다. `session-persistence.spec.ts`는 Phase 3 pre/post 3척도 입력이 브라우저 리뷰 UI → `PUT /users/me/prepost-measures` → DB-backed `GET /users/me/prepost-measures` → 새로고침 후 UI 복원까지 실제 API/DB로 이어지는지 검증한다. C3 수동 재평가는 persona 기본 `theory_target`이 학습자가 선택한 session `theory_mode`를 덮어쓰던 우선순위 오류를 수정했고, turn 재평가도 `TurnContext.theory_mode`를 채우도록 회귀화했다. 서브에이전트 병렬 조사 결과 남은 큰 공백은 C1 워크시트 learner-save/teacher-review full browser+API+DB E2E, C2 safety_events→교수자 대시보드 E2E, H2 live-coach evidence modal/source-pack actual sync proof였다. 검증: `python -X utf8 -m pytest -p no:cacheprovider app/test_eval_routes.py app/test_notifications.py app/test_session_turn_persistence.py -q` 39 passed, `PLAYWRIGHT_PORT=5205 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1` 7 passed, `npx playwright test --list` 현재 162 tests / 18 files.
@ -63,7 +63,7 @@
- [ ] **운영 티켓 자동 분류·처리 후속** — Claude Recipe headless 자동 수정 후보, 관리자 승인 후 이슈 등록·PR/작업 스레드 생성, 처리 결과 audit trail 확장은 아직 설계/승인 필요. 담당 그룹 자동 배정·우선순위 escalation·raw/rollup 보존기간 같은 운영 정책은 B3 결정 항목에서 먼저 닫아야 한다. 자동 수정은 운영자 승인 전까지 실행하지 않는다.
- [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`(현재 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로 별도 기록한다.
검증 기준: 변경 후 `npm run typecheck`, `e2e/layout-visual-gate.spec.ts`(현재 12/12), 레이아웃 포커스 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로 별도 기록한다.
---
@ -112,7 +112,7 @@
## 이번 세션에 닫은 것(참고)
- 레이아웃 시각 수용: 기본 게이트 7/7 + 적대적 재검수 7/7 accept에 더해 빈 회기리뷰 전용 gate를 추가한 현재 9/9 — DONE.
- 레이아웃 시각 수용: 기본 게이트 7/7 + 적대적 재검수 7/7 accept에 더해 빈 회기리뷰, 교수자 리뷰, 학생 분석 목록/상세 gate를 추가한 현재 12/12 — 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/ws``stt_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 항목에 계속 남긴다.