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:
Yun Chan 2026-08-08 01:30:53 +09:00
parent 93dd8f82d7
commit 16e791e044
390 changed files with 243188 additions and 499 deletions

View 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);
});
});