untracked files on imp47b-recovery: e46b4c5 feat(#76): IMP-47B reject-as-AI-adaptation activation (u1~u13 backend + tests)
This commit is contained in:
@@ -0,0 +1,181 @@
|
|||||||
|
/**
|
||||||
|
* Design Agent - Mock Data
|
||||||
|
*
|
||||||
|
* 이 파일은 백엔드 파이프라인이 연결되기 전까지 사용하는 mock 데이터입니다.
|
||||||
|
* Phase Z 설계 규격을 따릅니다.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type {
|
||||||
|
NormalizedContent,
|
||||||
|
SlidePlan,
|
||||||
|
LayoutCandidate,
|
||||||
|
FrameCandidate,
|
||||||
|
} from "../types/designAgent";
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Layout Candidates
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// 8 preset = backend templates/phase_z2/layouts/layouts.yaml 1:1 매칭.
|
||||||
|
// id / 순서 / 의미 모두 backend 와 동일해야 함.
|
||||||
|
export const MOCK_LAYOUT_CANDIDATES: LayoutCandidate[] = [
|
||||||
|
{ id: "single", name: "단일 본문", type: "full", description: "전체 영역 1 zone (primary)" },
|
||||||
|
{ id: "horizontal-2", name: "상/하 2단", type: "top-bottom", description: "위/아래 2 zone (top, bottom) — rows topology" },
|
||||||
|
{ id: "vertical-2", name: "좌/우 2단", type: "left-right", description: "좌/우 2 zone (left, right) — cols topology" },
|
||||||
|
{ id: "top-1-bottom-2", name: "상 1 : 하 2", type: "asymmetric", description: "위 1 zone + 아래 좌/우 2 zone — T topology" },
|
||||||
|
{ id: "top-2-bottom-1", name: "상 2 : 하 1", type: "asymmetric", description: "위 좌/우 2 zone + 아래 1 zone — inverted-T topology" },
|
||||||
|
{ id: "left-1-right-2", name: "좌 1 : 우 2", type: "asymmetric", description: "좌 1 zone + 우 상/하 2 zone — side-T-left topology" },
|
||||||
|
{ id: "left-2-right-1", name: "좌 2 : 우 1", type: "asymmetric", description: "좌 상/하 2 zone + 우 1 zone — side-T-right topology" },
|
||||||
|
{ id: "grid-2x2", name: "2x2 그리드", type: "grid", description: "4 zone 균등 배치 — 2x2 topology" },
|
||||||
|
];
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Frame Candidates (샘플 이미지 포함 - 8종 세트)
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const SAMPLE_FRAMES: FrameCandidate[] = [
|
||||||
|
{
|
||||||
|
id: "frame-001",
|
||||||
|
name: "3-Card 프로세스",
|
||||||
|
score: 0.98,
|
||||||
|
confidence: "high",
|
||||||
|
label: "use_as_is",
|
||||||
|
thumbnailUrl: "https://images.unsplash.com/photo-1614850523296-d8c1af93d400?w=300&h=200&fit=crop",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "frame-002",
|
||||||
|
name: "비교 테이블 (Light)",
|
||||||
|
score: 0.85,
|
||||||
|
confidence: "medium",
|
||||||
|
label: "light_edit",
|
||||||
|
thumbnailUrl: "https://images.unsplash.com/photo-1508615039623-a25651266b91?w=300&h=200&fit=crop",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "frame-003",
|
||||||
|
name: "계층형 트리",
|
||||||
|
score: 0.72,
|
||||||
|
confidence: "medium",
|
||||||
|
label: "restructure",
|
||||||
|
thumbnailUrl: "https://images.unsplash.com/photo-1551288049-bbda03a24d5d?w=300&h=200&fit=crop",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "frame-004",
|
||||||
|
name: "4-Grid 핵심 요약",
|
||||||
|
score: 0.65,
|
||||||
|
confidence: "low",
|
||||||
|
label: "reject",
|
||||||
|
thumbnailUrl: "https://images.unsplash.com/photo-1460925895917-afdab827c52f?w=300&h=200&fit=crop",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "frame-005",
|
||||||
|
name: "타임라인 로드맵",
|
||||||
|
score: 0.91,
|
||||||
|
confidence: "high",
|
||||||
|
label: "use_as_is",
|
||||||
|
thumbnailUrl: "https://images.unsplash.com/photo-1531403009284-440f080d1e12?w=300&h=200&fit=crop",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "frame-006",
|
||||||
|
name: "순환형 다이어그램",
|
||||||
|
score: 0.78,
|
||||||
|
confidence: "medium",
|
||||||
|
label: "light_edit",
|
||||||
|
thumbnailUrl: "https://images.unsplash.com/photo-1557804506-669a67965ba0?w=300&h=200&fit=crop",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "frame-007",
|
||||||
|
name: "수치 강조 카드",
|
||||||
|
score: 0.88,
|
||||||
|
confidence: "high",
|
||||||
|
label: "use_as_is",
|
||||||
|
thumbnailUrl: "https://images.unsplash.com/photo-1551288049-bbda03a24d5d?w=300&h=200&fit=crop",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "frame-008",
|
||||||
|
name: "좌우 대비 분석",
|
||||||
|
score: 0.62,
|
||||||
|
confidence: "low",
|
||||||
|
label: "restructure",
|
||||||
|
thumbnailUrl: "https://images.unsplash.com/photo-1454165833767-02a6ed8a687a?w=300&h=200&fit=crop",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const MOCK_FRAME_CANDIDATES_SECTION1 = SAMPLE_FRAMES;
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Normalized Content
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const MOCK_NORMALIZED_CONTENT: NormalizedContent = {
|
||||||
|
title: "DX 실행 체계 구축 방안",
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
id: "section-1",
|
||||||
|
index: 1,
|
||||||
|
level: 2,
|
||||||
|
title: "DX 실행 필수 요건",
|
||||||
|
content_objects: [
|
||||||
|
{ id: "co-1", type: "text_block", role: "summary", raw_payload: "조직 전반의 DX 역량 강화 필요", size_estimate: { line_count: 2 } },
|
||||||
|
{ id: "co-2", type: "text_block", role: "detail", raw_payload: "전담 조직 구성 및 KPI 설정", size_estimate: { line_count: 3 } }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Slide Plan (Phase Z 구조)
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const MOCK_SLIDE_PLAN: SlidePlan = {
|
||||||
|
id: "slide-001",
|
||||||
|
title: "DX 실행 체계 구축 방안",
|
||||||
|
layout_preset: "horizontal-2",
|
||||||
|
zones: [
|
||||||
|
{
|
||||||
|
id: "zone-left",
|
||||||
|
zone_id: "left",
|
||||||
|
section_ids: ["section-1"],
|
||||||
|
position: { x: 0, y: 0, width: 0.5, height: 1 },
|
||||||
|
internal_regions: [
|
||||||
|
{
|
||||||
|
id: "region-l1",
|
||||||
|
region_id: "content-1",
|
||||||
|
role: "primary",
|
||||||
|
content_type: "mixed",
|
||||||
|
ratio_estimate: 1,
|
||||||
|
content_unit_ids: ["co-1", "co-2"],
|
||||||
|
frame_match_strategy: {
|
||||||
|
kind: "frame_match",
|
||||||
|
frame_id: "frame-001",
|
||||||
|
display_strategy: "inline_full"
|
||||||
|
},
|
||||||
|
frame_candidates: SAMPLE_FRAMES
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "zone-right",
|
||||||
|
zone_id: "right",
|
||||||
|
section_ids: ["section-1"],
|
||||||
|
position: { x: 0.5, y: 0, width: 0.5, height: 1 },
|
||||||
|
internal_regions: [
|
||||||
|
{
|
||||||
|
id: "region-r1",
|
||||||
|
region_id: "content-2",
|
||||||
|
role: "secondary",
|
||||||
|
content_type: "text_block",
|
||||||
|
ratio_estimate: 1,
|
||||||
|
content_unit_ids: [],
|
||||||
|
frame_match_strategy: {
|
||||||
|
kind: "frame_match",
|
||||||
|
frame_id: "frame-002",
|
||||||
|
display_strategy: "inline_full"
|
||||||
|
},
|
||||||
|
frame_candidates: SAMPLE_FRAMES
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString()
|
||||||
|
};
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
// IMP-47B u11 — Frontend ai_repair_status notification surfacing.
|
||||||
|
//
|
||||||
|
// Scope (Stage 2 unit u11 contract):
|
||||||
|
// 1) loadRun → RunMeta.ai_repair_status exposes the u8 step20 payload.
|
||||||
|
// 2) formatAiRepairHumanReviewMessage(...) returns user-facing notification
|
||||||
|
// text on the three failure axes (error / coverage_violated /
|
||||||
|
// unsupported_kind) and returns null on success / no-AI paths.
|
||||||
|
//
|
||||||
|
// Pure-function unit test (no React Testing Library required — vitest is
|
||||||
|
// already in devDependencies; @testing-library/* is NOT installed). The
|
||||||
|
// Home.tsx wiring is a 2-line site that calls this helper after
|
||||||
|
// setRunMeta(...); covering the helper covers the user-visible message text
|
||||||
|
// directly without DOM rendering.
|
||||||
|
//
|
||||||
|
// File extension is `.tsx` per Stage 2 unit contract path; no JSX is required
|
||||||
|
// for these assertions but the extension allows future RTL-based tests to
|
||||||
|
// land here without renaming.
|
||||||
|
|
||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import {
|
||||||
|
formatAiRepairHumanReviewMessage,
|
||||||
|
type AiRepairStatus,
|
||||||
|
} from "../src/services/designAgentApi";
|
||||||
|
|
||||||
|
const baseCounts = {
|
||||||
|
total: 1,
|
||||||
|
applied: 0,
|
||||||
|
no_proposal: 0,
|
||||||
|
no_zone_match: 0,
|
||||||
|
unsupported_kind: 0,
|
||||||
|
error: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("formatAiRepairHumanReviewMessage (IMP-47B u11)", () => {
|
||||||
|
it("returns null when ai_repair_status is null (legacy / pre-Step12 abort)", () => {
|
||||||
|
expect(formatAiRepairHumanReviewMessage(null)).toBeNull();
|
||||||
|
expect(formatAiRepairHumanReviewMessage(undefined)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns null when human_review_required=false (success / no-AI path)", () => {
|
||||||
|
const ok: AiRepairStatus = {
|
||||||
|
status: "ok",
|
||||||
|
counts: { ...baseCounts, total: 0 },
|
||||||
|
unsupported_kind_records: [],
|
||||||
|
error_records: [],
|
||||||
|
coverage_status: "ok",
|
||||||
|
dropped_section_ids: [],
|
||||||
|
human_review_required: false,
|
||||||
|
};
|
||||||
|
expect(formatAiRepairHumanReviewMessage(ok)).toBeNull();
|
||||||
|
|
||||||
|
const applied: AiRepairStatus = {
|
||||||
|
...ok,
|
||||||
|
status: "applied",
|
||||||
|
counts: { ...baseCounts, total: 1, applied: 1 },
|
||||||
|
};
|
||||||
|
expect(formatAiRepairHumanReviewMessage(applied)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("surfaces AI call failures with count + frame/manual guidance", () => {
|
||||||
|
const errored: AiRepairStatus = {
|
||||||
|
status: "error",
|
||||||
|
counts: { ...baseCounts, total: 2, error: 2 },
|
||||||
|
unsupported_kind_records: [],
|
||||||
|
error_records: [
|
||||||
|
{ unit_index: 0, source_section_ids: ["03-1"], error: "timeout" },
|
||||||
|
{ unit_index: 1, source_section_ids: ["03-2"], error: "validation" },
|
||||||
|
],
|
||||||
|
coverage_status: "ok",
|
||||||
|
dropped_section_ids: [],
|
||||||
|
human_review_required: true,
|
||||||
|
};
|
||||||
|
const msg = formatAiRepairHumanReviewMessage(errored);
|
||||||
|
expect(msg).not.toBeNull();
|
||||||
|
expect(msg).toContain("AI 재구성 호출 실패");
|
||||||
|
expect(msg).toContain("2");
|
||||||
|
expect(msg).toContain("다른 frame 선택 또는 수동 편집 필요");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("surfaces coverage violations with the dropped section ids", () => {
|
||||||
|
const dropped: AiRepairStatus = {
|
||||||
|
status: "coverage_violated",
|
||||||
|
counts: { ...baseCounts, total: 1, applied: 1 },
|
||||||
|
unsupported_kind_records: [],
|
||||||
|
error_records: [],
|
||||||
|
coverage_status: "violated",
|
||||||
|
dropped_section_ids: ["03-2"],
|
||||||
|
human_review_required: true,
|
||||||
|
};
|
||||||
|
const msg = formatAiRepairHumanReviewMessage(dropped);
|
||||||
|
expect(msg).not.toBeNull();
|
||||||
|
expect(msg).toContain("콘텐츠 누락");
|
||||||
|
expect(msg).toContain("03-2");
|
||||||
|
expect(msg).toContain("다른 frame 선택 또는 수동 편집 필요");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("surfaces unsupported proposal kinds with the unsupported count", () => {
|
||||||
|
const unsupported: AiRepairStatus = {
|
||||||
|
status: "unsupported_kind",
|
||||||
|
counts: { ...baseCounts, total: 1, unsupported_kind: 1 },
|
||||||
|
unsupported_kind_records: [
|
||||||
|
{
|
||||||
|
unit_index: 0,
|
||||||
|
source_section_ids: ["03-1"],
|
||||||
|
apply_status: "unsupported_kind_for_reject_route:builder_options_patch",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
error_records: [],
|
||||||
|
coverage_status: "ok",
|
||||||
|
dropped_section_ids: [],
|
||||||
|
human_review_required: true,
|
||||||
|
};
|
||||||
|
const msg = formatAiRepairHumanReviewMessage(unsupported);
|
||||||
|
expect(msg).not.toBeNull();
|
||||||
|
expect(msg).toContain("AI 제안 형식 미지원");
|
||||||
|
expect(msg).toContain("1");
|
||||||
|
expect(msg).toContain("다른 frame 선택 또는 수동 편집 필요");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to a generic human_review message on unknown status enums", () => {
|
||||||
|
const future: AiRepairStatus = {
|
||||||
|
status: "future_axis_not_yet_mapped",
|
||||||
|
counts: { ...baseCounts, total: 0 },
|
||||||
|
unsupported_kind_records: [],
|
||||||
|
error_records: [],
|
||||||
|
coverage_status: "ok",
|
||||||
|
dropped_section_ids: [],
|
||||||
|
human_review_required: true,
|
||||||
|
};
|
||||||
|
const msg = formatAiRepairHumanReviewMessage(future);
|
||||||
|
expect(msg).not.toBeNull();
|
||||||
|
expect(msg).toContain("human_review");
|
||||||
|
expect(msg).toContain("future_axis_not_yet_mapped");
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user