학생 분석 목록 펼침 보강

This commit is contained in:
Yun Chan 2026-07-01 12:41:17 +09:00
parent 51053af536
commit 69fece56bf
9 changed files with 289 additions and 145 deletions

View file

@ -518,7 +518,7 @@
<p class="dg-note">82차 적용(2026-06-29): refactor-governance P8로 <code>Session.tsx</code>의 음성/세션 UI 파생 상태 계산을 <code>sessionVoiceStatusView()</code> helper로 분리했다. 마이크 busy/label, 세션 상태 label, transcript live label, 텍스트 입력 차단, 응답/음성 상태 chip 계산을 named view-model 경계에서 유지한다. API/DB 계약, 세션 시작·전송·종료, voice WebSocket/TTS lifecycle, live-coach payload, DOM/CSS는 변경하지 않았다. 검증: <code>npm run typecheck</code>, <code>npm run build</code>, <code>session-mvp</code> 1 passed, <code>voice-success</code> 2 passed, <code>session-layout</code> desktop/mobile 8 passed.</p>
<p class="dg-note">83차 적용(2026-06-29): refactor-governance P3로 학습자/교수자 성장 지표가 턴 평가 기법 라벨을 읽는 경계를 <code>session_metrics.turn_technique_label()</code>로 명시했다. DB 정규화 후 재수화되는 <code>TechniqueTag.label_ko</code>를 우선 사용하고, 기존 <code>label/name/id/code</code> legacy fallback은 유지한다. <code>alternative_utterances</code>는 future adapter 추출 전에 문자열 배열과 <code>text/suggestion</code> dict 배열을 모두 row로 정규화하는 호환 테스트로 고정했다. 검증: py_compile, evaluation/learner/teacher focused 59 passed.</p>
<p class="dg-note">84차 적용(2026-06-29): refactor-governance P2로 <code>Session.tsx</code>의 음성 guard/view-model 경계를 한 번 더 줄였다. <code>isVoiceStatusBusy()</code>가 마이크 busy 판정을 소유하고, <code>sessionVoiceStatusView()</code>가 마이크 disabled와 aria label까지 반환해 JSX와 <code>toggleMic()</code>의 중복 조건을 제거했다. <code>recording</code>은 계속 busy가 아니므로 두 번째 마이크 클릭으로 발화 전송이 가능하다. API/DB 계약, WebSocket/TTS lifecycle, STT/TTS provider 동작, live-coach payload, DOM/CSS는 변경하지 않았다. 검증: <code>npm run typecheck</code>, <code>npm run build</code>, <code>session-mvp</code> 1 passed, <code>voice-success</code> 2 passed, <code>session-layout</code> desktop/mobile 8 passed.</p>
<p class="dg-note">85차 적용(2026-06-29): refactor-governance P3로 Node 전환 계약 검증의 <code>gateway-default</code> sentinel 중복을 줄였다. <code>scripts/check-engine-gateway-contract.mjs</code>는 더 이상 sentinel 문자열을 자체 상수로 들고 있지 않고, <code>engine_gateway_schema.v1.json</code><code>x-engine-gateway-default-model-sentinel</code> extension을 읽어 schema artifact를 SSOT로 검증한다. 공개 런타임 SSOT도 재확인해 custom domain asset을 <code>assets/index-DgN8JVBw.js</code>/<code>assets/index--s3xqgO5.css</code>로 갱신했고, OAuth start는 동작하지만 인증된 <code>/turn</code> proof는 여전히 owner storageState gate로 남겼다. <code>vnet.18ka.net</code>/<code>api-vnet.18ka.net</code> DNS A record는 0건이라 DONE 처리하지 않는다.</p>
<p class="dg-note">85차 적용(2026-06-29): refactor-governance P3로 Node 전환 계약 검증의 <code>gateway-default</code> sentinel 중복을 줄였다. <code>scripts/check-engine-gateway-contract.mjs</code>는 더 이상 sentinel 문자열을 자체 상수로 들고 있지 않고, <code>engine_gateway_schema.v1.json</code><code>x-engine-gateway-default-model-sentinel</code> extension을 읽어 schema artifact를 SSOT로 검증한다. 공개 런타임 SSOT도 재확인해 custom domain asset을 <code>assets/index-Cz4PBoO3.js</code>/<code>assets/index-DtVTvS9j.css</code>로 갱신했고, OAuth start는 동작하지만 인증된 <code>/turn</code> proof는 여전히 owner storageState gate로 남겼다. <code>vnet.18ka.net</code>/<code>api-vnet.18ka.net</code> DNS A record는 0건이라 DONE 처리하지 않는다.</p>
<p class="dg-note">86차 적용(2026-06-29): refactor-governance P2/P3로 Session mic capture를 <code>AudioWorklet</code>-first 구조로 전환하고 기존 <code>MediaRecorder</code>를 fallback으로 남겼다. Worklet은 mono PCM16 chunk를 <code>/voice/ws</code> binary frame으로 보내고, control packet은 <code>format=pcm</code>, <code>sample_rate</code>, <code>channels</code>, <code>sample_width</code>를 명시한다. 백엔드는 <code>pcm</code> 업로드를 STT 호출 직전 RIFF/WAV로 정규화해 raw PCM을 <code>audio/wav</code>로 오인하던 계약 틈을 닫았다. 실제 Deepgram streaming, 물리 마이크, public WSS, 50분 양방향 실측은 계속 외부/실측 gate다. 검증: <code>app/test_voice_ws.py</code> 11 passed, <code>npm run typecheck</code>, <code>voice-success</code> chromium-single-run 2 passed.</p>
<p class="dg-note">87차 적용(2026-06-29): refactor-governance P3로 <code>/voice/ws</code>에 provider-neutral <code>stt_result</code> control을 추가해 EOT helper를 route 경계에 연결했다. 새 control은 <code>text</code>, <code>final</code>, <code>silence_ms</code>, <code>provider_events</code>를 받아 <code>assess_end_of_turn()</code>이 ready를 판단하고, pending이면 턴 실행 없이 <code>state=listening</code>으로 돌아간다. ready일 때만 transcript를 emit하고 <code>_run_turn_and_speak()</code>로 진행한다. 기존 <code>audio_start</code>→binary→<code>audio_end</code>, <code>text_turn</code>, ping/close 계약은 유지했다. 실제 Deepgram interim/final WSS 연결과 public WSS/물리 마이크/50분 실측은 여전히 남은 gate다. 검증: <code>py_compile</code>, <code>app/test_voice_ws.py app/test_voice_service.py</code> 27 passed.</p>
<p class="dg-note">88차 적용(2026-06-29): refactor-governance P3로 Node conformance runner의 stream event/payload 검증을 schema-driven으로 바꿨다. <code>scripts/check-engine-gateway-contract.mjs</code>는 더 이상 <code>token/done/error</code> 이벤트 목록과 payload 필드를 로컬 상수·분기로 들고 있지 않고, <code>engine_gateway_schema.v1.json</code><code>StreamPacket.oneOf</code>, payload <code>$ref</code>, <code>x-engine-gateway-sse-events</code>를 읽어 golden fixture를 검증한다. raw token text fallback과 <code>data: [DONE]</code> compatibility line은 유지했다. 검증: <code>node scripts\check-engine-gateway-contract.mjs --json</code>, 당시 gateway/backend focused suite 통과, 이후 최신 L1 focused 120 passed로 재검증.</p>
@ -548,6 +548,7 @@
<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">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>
<p class="dg-note">25차 적용(2026-06-28): <code>/admin/users</code>에서 허용 도메인 밖 이메일도 정확한 계정 단위로 강제 등록할 수 있다. Google/SAML/dev-login은 미리 등록된 이메일만 도메인 게이트 예외로 통과시키고, provider 로그인 시 기존 관리 row의 역할·코호트·승인 상태를 이어받는다. 미등록 외부 도메인 로그인은 계속 차단한다.</p>
@ -918,7 +919,7 @@
<thead><tr><th>범위</th><th>명령 / 확인 대상</th><th>결과</th></tr></thead>
<tbody>
<tr><td>Web typecheck</td><td><code>npm run typecheck</code></td><td>Passed</td></tr>
<tr><td>Full Playwright E2E baseline</td><td><code>npx playwright test --list</code></td><td>현재 수집 기준 162 tests / 18 files. 최신 변경은 focused gate 행으로 별도 검증한다.</td></tr>
<tr><td>Full Playwright E2E baseline</td><td><code>npx playwright test --list</code></td><td>현재 수집 기준 166 tests / 18 files. 최신 변경은 focused gate 행으로 별도 검증한다.</td></tr>
<tr><td>API typegen SSOT</td><td><code>npm run check:api-types</code></td><td>Passed; FastAPI OpenAPI → <code>src/lib/api.gen.ts</code> stale check</td></tr>
<tr><td>Contract SSOT aggregate DTO</td><td><code>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</code> / <code>npm run check:api-types</code> / <code>npm run typecheck</code> / <code>npm run build</code></td><td>50 backend passed; learner sessions, session review/worksheet, teacher dashboard, session start/detail DTOs use generated <code>ApiSchema</code> aliases with UI fallback. Stage responses are OpenAPI enum unions, including review phase key/label, reached phase, evaluation summary/trigger responses, and teacher session/growth stage. <code>.github/workflows/api-contract.yml</code> 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>
@ -931,10 +932,10 @@
<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>L1 persona read/generation split</td><td><code>py -3.11 -X utf8 -B -m py_compile app/persona_generation_contract.py app/persona_read_model.py app/routes/personas.py app/test_persona_generation_contract.py app/test_persona_review.py</code> / <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> / <code>npm run check:api-types</code> / <code>npm run typecheck</code></td><td>py_compile passed; persona contract+review 39 passed; OpenAPI generated API type check and web typecheck passed. <code>app/persona_read_model.py</code> owns persona catalog/review/draft/source/evidence DTOs and mappers, while <code>app/persona_generation_contract.py</code> owns draft schema, prompt bundle, <code>GenerateResponse</code> payload extraction, and generated draft defaults/coercion. <code>routes/personas.py</code> keeps auth, teacher/admin gates, DB repository calls, RAG source registration, engine invocation, provenance assembly, and HTTP error mapping.</td></tr>
<tr><td>M1 provider_events preservation</td><td><code>python -X utf8 -m pytest -p no:cacheprovider app/test_runtime_policy.py app/test_session_turn_persistence.py app/test_voice_ws.py app/test_voice_service.py -q</code></td><td>77 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은 응답하지 않는다. Runtime readiness now requires all five turn voice metadata columns.</td></tr>
<tr><td>C1 case worksheet</td><td><code>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</code> / <code>scripts/check-case-worksheet-rubric.py --json</code> / <code>npm run check:api-types</code> / <code>npm run typecheck</code> / <code>npm run build</code> / <code>session-review.spec.ts</code> / local API smoke</td><td>40 passed for worksheet persistence/RBAC/teacher review plus rubric scaffold; session review exposes transcript-grounded <code>caseWorksheet</code>, learner-edited <code>saved_by_learner</code> worksheet persists through <code>app.case_worksheet</code>, teacher/admin <code>teacherReview</code> exposes manual <code>worksheetStatus</code>/<code>worksheetNote</code>/<code>worksheetReviewedAt</code>, and <code>case-worksheet-rubric@scaffold-2026-06-28</code> validates 5 sections/28 items with <code>scoring_enabled=false</code>. 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>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</code> / <code>scripts/check-case-worksheet-rubric.py --json</code> / <code>PLAYWRIGHT_PORT=5228 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1 --grep "case worksheet"</code> / local API smoke</td><td>40 passed for worksheet persistence/RBAC/teacher review plus rubric scaffold; session review exposes transcript-grounded <code>caseWorksheet</code>, learner-edited <code>saved_by_learner</code> worksheet persists through <code>app.case_worksheet</code>, teacher/admin <code>teacherReview</code> exposes manual <code>worksheetStatus</code>/<code>worksheetNote</code>/<code>worksheetReviewedAt</code>, and full browser+API+DB E2E now verifies learner save → teacher read-only review → <code>수정요청</code> persistence. <code>case-worksheet-rubric@scaffold-2026-06-28</code> still validates 5 sections/28 items with <code>scoring_enabled=false</code>.</td></tr>
<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></td><td>20 passed; real crisis stops before engine, returns 109 resource, safety event DB insert payload covered</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>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>
@ -946,7 +947,7 @@
<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>47 passed; 최신 보정 회귀 59 passed. phone/email/RRN 및 한국어 NAME/ORG raw 값이 generate/stream/evaluator payload와 client <code>text_masked</code>에 남지 않음. Optional ko recognizer fake span은 <code>[NAME]</code>/<code>[ORG]</code>로 마스킹되고 같은 문장의 phone은 후단 regex가 처리하며, adapter 실패 시에도 regex fallback이 유지된다. Synthetic ko fixture 16/16 pass, 자연 발화형 이름 라벨·자기소개와 negative control 포함, input/report schema validation, summary-only report(<code>evidence_text_included=false</code>), entity recall 1.0, forbidden substring removal 1.0, unexpected entity violations 0.</td></tr>
<tr><td>P2a RBAC/audit/visibility</td><td><code>app.test_rbac_idor</code></td><td>11 tests OK; other learner 403, read_session audit, evaluator-only hidden, teacher session review read allowed, super_admin learner review principal is promoted to admin for evaluation/worksheet/review-status loaders, admin_access-only learner remains blocked, and learner worksheet write remains 403</td></tr>
<tr><td>Admin access delegation</td><td><code>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</code> / <code>npx playwright test e2e/admin.spec.ts --project=chromium-desktop --workers=1</code> / <code>npm run check:api-types</code> / <code>npm run typecheck</code> / <code>npm run build</code></td><td>70 backend focused passed + admin/auth/runtime 64 passed + admin E2E 8 passed. 실제 <code>admin</code> role은 학습자·교수자·관리자 공간을 모두 열고, <code>admin_access</code>만 받은 비관리자 계정은 관리자 콘솔 진입권만 얻는다. 관리자 sidebar는 운영 홈/사용자/권한/티켓과 교수자·학습자 작업 공간 링크를 함께 노출한다.</td></tr>
<tr><td>Session read-model DB readiness</td><td><code>python scripts\check-deploy-preflight.py --skip-db --env-file infra\.env.example --allow-placeholder-secrets</code> / <code>https://api-vignette.chanpaca.net/health</code></td><td>2026-06-29 prod 503 원인은 운영 DB의 <code>app.turns.provider_events</code> 컬럼 누락이었다. 운영 DB hotfix 후 <code>/teacher/dashboard</code> code path는 <code>source=database</code>로 복구됐다. 현재 <code>db.healthcheck()</code>, runtime table readiness, deploy preflight DB mode는 <code>app.turns</code> 음성 메타 컬럼 5개(<code>audio_ref</code>, <code>silence_ms</code>, <code>speech_rate</code>, <code>barge_in</code>, <code>provider_events</code>) <code>app.session_review_status</code> worksheet 컬럼을 함께 검증한다. 2026-06-30에는 Docker Desktop/DB 중단으로 public API 530/error code 1033이 재발했지만, Docker Desktop/DB 재기동 뒤 public health가 <code>environment=prod</code>, <code>db=true</code>, <code>engine=true</code>로 복구됐다.</td></tr>
<tr><td>Session read-model DB readiness</td><td><code>python scripts\check-deploy-preflight.py --skip-db --env-file infra\.env.example --allow-placeholder-secrets</code> / <code>https://api-vignette.chanpaca.net/health</code></td><td>2026-06-29 prod 503 원인은 운영 DB의 <code>app.turns.provider_events</code> 컬럼 누락이었다. 운영 DB hotfix 후 <code>/teacher/dashboard</code> code path는 <code>source=database</code>로 복구됐다. 현재 <code>db.healthcheck()</code>, runtime table readiness, deploy preflight DB mode는 <code>app.turns</code> 음성 메타 컬럼 5개(<code>audio_ref</code>, <code>silence_ms</code>, <code>speech_rate</code>, <code>barge_in</code>, <code>provider_events</code>), <code>app.session_review_status</code> worksheet 컬럼, <code>app.safety_events</code> 필수 컬럼을 함께 검증한다. 2026-06-30에는 Docker Desktop/DB 중단으로 public API 530/error code 1033이 재발했지만, Docker Desktop/DB 재기동 뒤 public health가 <code>environment=prod</code>, <code>db=true</code>, <code>engine=true</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>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>