G0~G8 성과·동맹 측정 OS 작업 일괄 고정
8월 7일까지 워킹트리에만 남아 있던 미커밋 작업을 커밋한다. 여러 사본 폴더(worktree·clone)에 흩어져 있던 중간 스냅샷을 정리하기 전에 원본을 git 이력으로 고정하는 것이 목적이다. - contracts/routes/services: measurement, outcome_trajectory, rupture_repair, deliberate_practice, calibration_transfer, supervision_research, multimodal_alliance, continuous_improvement 계열 신규 모듈과 테스트 - infra/db/init: 07~16 마이그레이션(측정 기반~calibration transfer 실행) - apps/web: 세션 리뷰 카드·관리 화면·E2E 스펙 추가 - docs/ops: G0~G8 라이브 통합·배포·롤백 증거 문서와 evidence JSON/PNG - scripts: smoke·ledger·릴리스 에이전트·NAS 프리뷰 운영 스크립트 engine.public 로그 .bak과 apps/web/test-results 산출물은 커밋에서 제외했다.
This commit is contained in:
parent
93dd8f82d7
commit
16e791e044
390 changed files with 243188 additions and 499 deletions
386
apps/web/e2e/supervision-research.spec.ts
Normal file
386
apps/web/e2e/supervision-research.spec.ts
Normal file
|
|
@ -0,0 +1,386 @@
|
|||
import { expect, test, type Page } from "@playwright/test";
|
||||
import type {
|
||||
ResearchViewResponse,
|
||||
SupervisionViewResponse,
|
||||
} from "../src/pages/supervisionResearchApi";
|
||||
import { expectNoHorizontalOverflow } from "./support";
|
||||
|
||||
const FORBIDDEN_VERBATIM = "내담자가 실제로 말한 비공개 원문";
|
||||
|
||||
function routeUnmockedApi(page: Page) {
|
||||
return page.route("**/api/**", (route) =>
|
||||
route.fulfill({
|
||||
status: 404,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({ detail: "not part of the focused G6 fixture" }),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
function routeAuth(page: Page, role: "teacher" | "admin") {
|
||||
return page.route("**/auth/me", (route) =>
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
user_id:
|
||||
role === "teacher"
|
||||
? "62000000-0000-0000-0000-000000000002"
|
||||
: "62000000-0000-0000-0000-000000000003",
|
||||
email: `${role}@hs.ac.kr`,
|
||||
role,
|
||||
display_name: role === "teacher" ? "E2E Teacher" : "E2E Research Admin",
|
||||
admin_access: role === "admin",
|
||||
super_admin: false,
|
||||
account_status: "approved",
|
||||
approval_required: false,
|
||||
cohort_ids: ["g6-cohort"],
|
||||
consent_at: 1782820000,
|
||||
onboarding_completed_at: 1782820001,
|
||||
nickname: role === "teacher" ? "E2E Teacher" : "E2E Research Admin",
|
||||
self_introduction: "",
|
||||
avatar_url: "",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
function supervisionFixture(): SupervisionViewResponse {
|
||||
return {
|
||||
attention_items: [
|
||||
{
|
||||
item_id: "g6-attention-risk",
|
||||
snapshot_id: "g6-snapshot-001",
|
||||
learner_id: "62000000-0000-0000-0000-000000000101",
|
||||
learner_ref: "learner-risk",
|
||||
cohort_id: "g6-cohort",
|
||||
queue_position: 1,
|
||||
primary_signal: "deterioration",
|
||||
oldest_active_sequence: 4,
|
||||
drilldown_routes: [
|
||||
"/teach/analysis?learner=learner-risk&tab=outcome",
|
||||
"/teach/analysis?learner=learner-risk&tab=safety",
|
||||
"/teach/session/g6-session-risk/review",
|
||||
],
|
||||
evidence_pointer_ids: [
|
||||
"pointer-outcome-risk",
|
||||
"pointer-safety-risk",
|
||||
"pointer-alliance-risk",
|
||||
"pointer-hidden-by-cap",
|
||||
],
|
||||
created_at: "2026-08-06T03:00:00Z",
|
||||
},
|
||||
{
|
||||
item_id: "g6-attention-stagnation",
|
||||
snapshot_id: "g6-snapshot-001",
|
||||
learner_id: "62000000-0000-0000-0000-000000000102",
|
||||
learner_ref: "learner-stagnation",
|
||||
cohort_id: "g6-cohort",
|
||||
queue_position: 2,
|
||||
primary_signal: "growth_stagnation",
|
||||
oldest_active_sequence: 7,
|
||||
drilldown_routes: ["/teach/analysis?learner=learner-stagnation&tab=practice"],
|
||||
evidence_pointer_ids: ["pointer-practice-stagnation"],
|
||||
created_at: "2026-08-06T03:00:00Z",
|
||||
},
|
||||
{
|
||||
item_id: "g6-attention-rupture",
|
||||
snapshot_id: "g6-snapshot-001",
|
||||
learner_id: "62000000-0000-0000-0000-000000000103",
|
||||
learner_ref: "learner-rupture",
|
||||
cohort_id: "g6-cohort",
|
||||
queue_position: 3,
|
||||
primary_signal: "unresolved_rupture",
|
||||
oldest_active_sequence: 9,
|
||||
drilldown_routes: ["/teach/analysis?learner=learner-rupture&tab=rupture"],
|
||||
evidence_pointer_ids: ["pointer-rupture-open"],
|
||||
created_at: "2026-08-06T03:00:00Z",
|
||||
},
|
||||
],
|
||||
curriculum_gaps: [
|
||||
{
|
||||
gap_snapshot_id: "g6-gap-001",
|
||||
cohort_id: "g6-cohort",
|
||||
competency_id: "competency.rupture-repair",
|
||||
gap_kind: "rupture_repair",
|
||||
status: "observed",
|
||||
uncertainty: 0.18,
|
||||
affected_learner_count: 4,
|
||||
evidence_pointer_ids: ["pointer-gap-1", "pointer-gap-2"],
|
||||
created_at: "2026-08-06T03:02:00Z",
|
||||
},
|
||||
{
|
||||
gap_snapshot_id: "g6-gap-002",
|
||||
cohort_id: "g6-cohort",
|
||||
competency_id: "competency.transfer-context",
|
||||
gap_kind: "transfer",
|
||||
status: "insufficient_evidence",
|
||||
uncertainty: 1,
|
||||
affected_learner_count: 0,
|
||||
evidence_pointer_ids: [],
|
||||
created_at: "2026-08-06T03:02:00Z",
|
||||
},
|
||||
],
|
||||
clinical_claim_allowed: false,
|
||||
};
|
||||
}
|
||||
|
||||
function researchFixture(): ResearchViewResponse {
|
||||
return {
|
||||
calibration_dataset: [
|
||||
{
|
||||
dataset_row_id: "g6-dataset-row-001",
|
||||
row_hash: "a".repeat(64),
|
||||
disagreement_record_id: "g6-disagreement-001",
|
||||
case_ref: "synthetic-case-001",
|
||||
competency_id: "competency.rupture-repair",
|
||||
ai_label: "resolved",
|
||||
teacher_label: "partial",
|
||||
ai_model: "evaluator-v2",
|
||||
prompt_version: "2.3.0",
|
||||
instrument_id: "rupture-repair-evaluator",
|
||||
instrument_version: "1.4.0",
|
||||
correction_reason_code: "repair_impact_not_confirmed",
|
||||
evidence_pointer_ids: ["pointer-ai-001", "pointer-teacher-001"],
|
||||
raw_transcript_included: false,
|
||||
created_at: "2026-08-06T03:04:00Z",
|
||||
},
|
||||
],
|
||||
drift_reports: [
|
||||
{
|
||||
drift_report_id: "g6-drift-001",
|
||||
cohort_id: "g6-cohort",
|
||||
matched_count: 12,
|
||||
status: "drift_flagged",
|
||||
baseline_accuracy: 0.83,
|
||||
candidate_accuracy: 0.67,
|
||||
accuracy_delta: -0.16,
|
||||
disagreement_case_refs: ["synthetic-case-b2", "synthetic-case-b3"],
|
||||
alerts: [
|
||||
"overall_accuracy_regression",
|
||||
"synthetic_subgroup_regression:synthetic-low-disclosure",
|
||||
],
|
||||
evidence_pointer_ids: ["pointer-drift-baseline", "pointer-drift-candidate"],
|
||||
created_at: "2026-08-06T03:06:00Z",
|
||||
baseline_model: "evaluator-v1",
|
||||
candidate_model: "evaluator-v2",
|
||||
baseline_prompt_version: "1.8.0",
|
||||
candidate_prompt_version: "2.3.0",
|
||||
instrument_id: "alliance-evaluation-suite",
|
||||
baseline_instrument_version: "1.1.0",
|
||||
candidate_instrument_version: "1.4.0",
|
||||
subgroup_metrics: [
|
||||
{
|
||||
subgroup: "synthetic-low-disclosure",
|
||||
matched_count: 4,
|
||||
baseline_accuracy: 0.75,
|
||||
candidate_accuracy: 0.5,
|
||||
accuracy_delta: -0.25,
|
||||
},
|
||||
{
|
||||
subgroup: "synthetic-high-resistance",
|
||||
matched_count: 4,
|
||||
baseline_accuracy: 0.75,
|
||||
candidate_accuracy: 0.75,
|
||||
accuracy_delta: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
phase3_manifests: [
|
||||
{
|
||||
manifest_id: "g6-manifest-001",
|
||||
cohort_id: "g6-cohort",
|
||||
schema_version: "vignette.phase3-outcome-evidence-manifest.v1",
|
||||
artifact_count: 4,
|
||||
created_at: "2026-08-06T03:08:00Z",
|
||||
artifacts: [
|
||||
{
|
||||
domain: "alliance",
|
||||
artifact_id: "artifact-alliance-v1",
|
||||
schema_version: "alliance.v1",
|
||||
content_sha256: "1".repeat(64),
|
||||
record_count: 24,
|
||||
provenance_uri: "db://measurement/alliance",
|
||||
clinical_claim_allowed: false,
|
||||
},
|
||||
{
|
||||
domain: "rupture",
|
||||
artifact_id: "artifact-rupture-v1",
|
||||
schema_version: "rupture.v1",
|
||||
content_sha256: "2".repeat(64),
|
||||
record_count: 18,
|
||||
provenance_uri: "db://measurement/rupture",
|
||||
clinical_claim_allowed: false,
|
||||
},
|
||||
{
|
||||
domain: "transfer",
|
||||
artifact_id: "artifact-transfer-v1",
|
||||
schema_version: "transfer.v1",
|
||||
content_sha256: "3".repeat(64),
|
||||
record_count: 16,
|
||||
provenance_uri: "audit://transfer/suite-v1",
|
||||
clinical_claim_allowed: false,
|
||||
},
|
||||
{
|
||||
domain: "calibration",
|
||||
artifact_id: "artifact-calibration-v1",
|
||||
schema_version: "calibration.v1",
|
||||
content_sha256: "4".repeat(64),
|
||||
record_count: 20,
|
||||
provenance_uri: "repo://evidence/calibration-v1",
|
||||
clinical_claim_allowed: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
raw_transcript_included: false,
|
||||
clinical_claim_allowed: false,
|
||||
};
|
||||
}
|
||||
|
||||
async function routeG6(
|
||||
page: Page,
|
||||
role: "teacher" | "admin",
|
||||
supervision = supervisionFixture(),
|
||||
research = researchFixture(),
|
||||
) {
|
||||
await routeUnmockedApi(page);
|
||||
await routeAuth(page, role);
|
||||
await page.route("**/api/supervision-research/supervision-view", (route) =>
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(supervision),
|
||||
}),
|
||||
);
|
||||
await page.route("**/api/supervision-research/research-view", (route) =>
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(research),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
test.describe("G6 supervision and research OS", () => {
|
||||
test("teacher follows a three-click-bounded evidence queue without research controls", async ({
|
||||
page,
|
||||
}) => {
|
||||
await routeG6(page, "teacher");
|
||||
await page.goto("/teach/supervision");
|
||||
await page.evaluate(() => document.documentElement.setAttribute("data-theme", "dark"));
|
||||
|
||||
await expect(
|
||||
page.getByRole("heading", {
|
||||
name: "먼저 볼 경로와, 믿을 수 있는 출처를 분리해 본다.",
|
||||
}),
|
||||
).toBeVisible();
|
||||
await expect(page.getByRole("tab", { name: "교수 감독" })).toHaveAttribute(
|
||||
"aria-selected",
|
||||
"true",
|
||||
);
|
||||
await expect(page.getByRole("tab", { name: "연구 품질" })).toHaveCount(0);
|
||||
await expect(page.getByText("위험", { exact: true })).toBeVisible();
|
||||
await expect(page.getByText("정체", { exact: true })).toBeVisible();
|
||||
await expect(page.getByText("미해결 관계 사건", { exact: true }).first()).toBeVisible();
|
||||
|
||||
const stagnation = page.getByRole("button", { name: /learner-stagnation/ });
|
||||
await stagnation.focus();
|
||||
await page.keyboard.press("Enter");
|
||||
await expect(stagnation).toHaveAttribute("aria-pressed", "true");
|
||||
await page.getByRole("button", { name: /learner-risk/ }).click();
|
||||
await expect(page.locator(".g6-ledger li")).toHaveCount(3);
|
||||
await expect(page.locator(".g6-drilldown a")).toHaveCount(3);
|
||||
await expect(page.getByTestId("drilldown-depth")).toContainText("계약 상한 3번");
|
||||
|
||||
await expect(page.getByRole("heading", { name: "개인의 순위를 만들지 않는 교육과정 공백" })).toBeVisible();
|
||||
await expect(page.getByText("competency.rupture-repair").first()).toBeVisible();
|
||||
await expect(page.getByText("근거 없는 상태를 유지하고 해석하지 않아.")).toBeVisible();
|
||||
await expect(page.getByText("resolved", { exact: true })).toBeVisible();
|
||||
await expect(page.getByText("partial", { exact: true })).toBeVisible();
|
||||
await expect(page.getByText("원문 축어록 제외")).toBeVisible();
|
||||
await expect(page.getByText(/XP|총점/i)).toHaveCount(0);
|
||||
await expect(page.getByRole("button", { name: /확정|승인|판정 저장/ })).toHaveCount(0);
|
||||
await expect(page.getByText(FORBIDDEN_VERBATIM)).toHaveCount(0);
|
||||
await expectNoHorizontalOverflow(page);
|
||||
});
|
||||
|
||||
test("research role traces model, prompt, instrument, subgroup, and four-domain provenance", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.emulateMedia({ reducedMotion: "reduce" });
|
||||
await routeG6(page, "admin");
|
||||
await page.goto("/teach/supervision");
|
||||
await page.getByRole("tab", { name: "연구 품질" }).click();
|
||||
|
||||
await expect(page.getByRole("heading", { name: "모델·프롬프트·도구·하위집단 드리프트" })).toBeVisible();
|
||||
await expect(page.getByText("evaluator-v1", { exact: true })).toBeVisible();
|
||||
await expect(
|
||||
page.getByLabel("모델 프롬프트 도구 버전 출처").getByText("evaluator-v2", { exact: true }),
|
||||
).toBeVisible();
|
||||
await expect(page.getByText("prompt 1.8.0", { exact: true })).toBeVisible();
|
||||
await expect(page.getByText("prompt 2.3.0", { exact: true })).toBeVisible();
|
||||
await expect(page.getByText("alliance-evaluation-suite", { exact: true })).toBeVisible();
|
||||
await expect(page.getByText("synthetic-low-disclosure", { exact: true })).toBeVisible();
|
||||
await expect(page.getByText("-25%p", { exact: true })).toBeVisible();
|
||||
|
||||
await expect(page.getByText("4/4 provenance 연결", { exact: true })).toBeVisible();
|
||||
for (const label of ["동맹", "파열·수선", "전이", "보정"]) {
|
||||
await expect(page.getByText(label, { exact: true }).first()).toBeVisible();
|
||||
}
|
||||
await expect(page.getByText("db://measurement/alliance", { exact: true })).toBeVisible();
|
||||
await expect(page.getByText("audit://transfer/suite-v1", { exact: true })).toBeVisible();
|
||||
await expect(page.getByText("repo://evidence/calibration-v1", { exact: true })).toBeVisible();
|
||||
await expect(page.getByText(FORBIDDEN_VERBATIM)).toHaveCount(0);
|
||||
await expect(page.getByText(/XP|총점/i)).toHaveCount(0);
|
||||
|
||||
const activeTab = page.getByRole("tab", { name: "연구 품질" });
|
||||
await expect(activeTab).toHaveAttribute("tabindex", "0");
|
||||
expect(
|
||||
await page.evaluate(() => window.matchMedia("(prefers-reduced-motion: reduce)").matches),
|
||||
).toBe(true);
|
||||
const reducedTransitionSeconds = await activeTab.evaluate((element) =>
|
||||
Number.parseFloat(window.getComputedStyle(element).transitionDuration),
|
||||
);
|
||||
expect(reducedTransitionSeconds).toBeLessThanOrEqual(0.00001);
|
||||
await expectNoHorizontalOverflow(page);
|
||||
});
|
||||
|
||||
test("empty and summary-only contracts stay explicit instead of inventing evidence", async ({
|
||||
page,
|
||||
}) => {
|
||||
const emptySupervision: SupervisionViewResponse = {
|
||||
attention_items: [],
|
||||
curriculum_gaps: [],
|
||||
clinical_claim_allowed: false,
|
||||
};
|
||||
const summaryOnlyResearch: ResearchViewResponse = {
|
||||
calibration_dataset: [],
|
||||
drift_reports: [],
|
||||
phase3_manifests: [
|
||||
{
|
||||
manifest_id: "g6-manifest-summary-only",
|
||||
cohort_id: "g6-cohort",
|
||||
schema_version: "vignette.phase3-outcome-evidence-manifest.v1",
|
||||
artifact_count: 4,
|
||||
created_at: "2026-08-06T03:08:00Z",
|
||||
},
|
||||
],
|
||||
raw_transcript_included: false,
|
||||
clinical_claim_allowed: false,
|
||||
};
|
||||
await routeG6(page, "admin", emptySupervision, summaryOnlyResearch);
|
||||
await page.goto("/teach/supervision");
|
||||
|
||||
await expect(page.getByText("현재 우선 검토 항목이 없어")).toBeVisible();
|
||||
await expect(page.getByText("현재 교육과정 공백이 없어")).toBeVisible();
|
||||
await expect(page.getByText("교수자–AI 불일치 메타데이터가 없어")).toBeVisible();
|
||||
await page.getByRole("tab", { name: "연구 품질" }).click();
|
||||
await expect(page.getByText("버전 드리프트 비교가 없어")).toBeVisible();
|
||||
await expect(page.getByText("provenance 저하", { exact: true })).toBeVisible();
|
||||
await expect(page.getByText("매니페스트 요약만 도착했어")).toBeVisible();
|
||||
await expect(page.getByText("원본 provenance 미제공").first()).toBeVisible();
|
||||
await expectNoHorizontalOverflow(page);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue