diff --git a/Front/client/src/components/FramePanel.tsx b/Front/client/src/components/FramePanel.tsx index c7f383a..9168c57 100644 --- a/Front/client/src/components/FramePanel.tsx +++ b/Front/client/src/components/FramePanel.tsx @@ -9,6 +9,7 @@ import { Badge } from '@/components/ui/badge'; import { motion } from 'framer-motion'; import type { Zone, InternalRegion, UserSelection, FrameCandidate, SlidePlan } from '../types/designAgent'; import { getSectionsForZone } from '../utils/slidePlanUtils'; +import { buildBadgeTitle } from '../services/applicationMode'; interface FramePanelProps { slidePlan: SlidePlan | null; @@ -235,6 +236,13 @@ export default function FramePanel({ )} {/* V4 label badge */} + {/* IMP-41 u5 — tooltip delegated to pure helper + `buildBadgeTitle` (services/applicationMode.ts). + applicationMode is forwarded by designAgentApi.ts + (u4) from Step 9 unit.application_candidates[]; + helper falls back to the raw V4 label when the + mode is undefined or unknown. Badge color mapping + is intentionally untouched per Stage 2 scope. */} {candidate.label && ( {candidate.label} diff --git a/Front/client/src/services/applicationMode.ts b/Front/client/src/services/applicationMode.ts new file mode 100644 index 0000000..5acbbac --- /dev/null +++ b/Front/client/src/services/applicationMode.ts @@ -0,0 +1,64 @@ +// ─── IMP-41 u2 — application_mode helper (issue #70) ──────────────────────── +// Pure deterministic helpers for forwarding backend Step 9 +// `unit.application_candidates[]` to the FramePanel V4-label badge tooltip. +// +// Keyed by backend `application_mode` VALUE (NOT V4 label) — preserves the +// AI-isolation contract: tooltip text is a read-only display of backend +// authority, never re-derived on the frontend from V4 label. +// +// Source of truth = src/phase_z2_pipeline.py APPLICATION_MODE_BY_V4_LABEL +// (:107-112) emitted via _application_candidates_for_unit() (:3071-3092) +// onto unit.application_candidates[] in step09_application_plan.json. + +/** Backend application_mode enumeration (verbatim from APPLICATION_MODE_BY_V4_LABEL). */ +export type ApplicationMode = + | 'direct_insert' + | 'same_frame_with_adjustment' + | 'layout_or_region_change' + | 'exclude'; + +/** Korean consequence phrases per issue #70 spec item #2. Keyed by mode VALUE. */ +export const APPLICATION_MODE_TOOLTIP_KR: Record = { + direct_insert: '코드 직접 적용', + same_frame_with_adjustment: 'AI 보강 필요', + layout_or_region_change: 'AI restructure 필요', + exclude: 'render path 제외', +}; + +/** + * Compose the V4-label badge tooltip title. When `applicationMode` resolves + * to a known mode the title shows the Korean consequence + raw mode token; + * otherwise (undefined or unknown — legacy fixtures pre-IMP-32) it falls + * back to the raw V4 label string per Stage 2 contract. + */ +export function buildBadgeTitle( + label: string, + applicationMode: string | undefined, +): string { + const consequence = applicationMode + ? APPLICATION_MODE_TOOLTIP_KR[applicationMode as ApplicationMode] + : undefined; + return consequence + ? `${consequence} (${applicationMode})` + : `V4 label: ${label}`; +} + +/** + * Build a Map from a Step 9 + * `unit.application_candidates[]` array. Entries with a non-string or empty + * `template_id` are skipped. First occurrence wins on duplicate keys. + * Pure — does NOT sort, slice, or filter by label/confidence. + */ +export function mergeApplicationCandidates( + applicationCandidates: unknown, +): Map { + const out = new Map(); + if (!Array.isArray(applicationCandidates)) return out; + for (const ac of applicationCandidates) { + const key = (ac as any)?.template_id; + if (typeof key === 'string' && key.length > 0 && !out.has(key)) { + out.set(key, ac); + } + } + return out; +} diff --git a/Front/client/src/services/designAgentApi.ts b/Front/client/src/services/designAgentApi.ts index 5db5ffa..eda5981 100644 --- a/Front/client/src/services/designAgentApi.ts +++ b/Front/client/src/services/designAgentApi.ts @@ -20,6 +20,8 @@ import { MOCK_FRAME_CANDIDATES_SECTION1, } from "../data/mockDesignAgentData"; +import { mergeApplicationCandidates } from "./applicationMode"; + /** 네트워크 지연 시뮬레이션 */ const simulateDelay = (ms: number = 800) => new Promise((resolve) => setTimeout(resolve, ms)); @@ -527,9 +529,20 @@ export async function loadRun(runId: string): Promise { if (lp !== 0) return lp; return (b.confidence ?? 0) - (a.confidence ?? 0); }); + // ─── IMP-41 u4 — application_candidates enrichment (issue #70) ─────────── + // Backend Step 9 emits `unit.application_candidates[]` (src/phase_z2_pipeline.py + // _application_candidates_for_unit, :3071-3092) one entry per v4 candidate with + // application_mode / auto_applicable / delegated_to derived from + // APPLICATION_MODE_BY_V4_LABEL (:107-112). Indexing delegated to the pure + // helper `mergeApplicationCandidates` (services/applicationMode.ts) keyed + // by template_id. Enrichment ONLY — does NOT alter candidate source + // priority, sorting, or TOP_N_FRAMES slicing. + const applicationModeMap = mergeApplicationCandidates(unit.application_candidates); const frameCandidates: FrameCandidate[] = v4Source .slice(0, TOP_N_FRAMES) - .map((c: any) => ({ + .map((c: any) => { + const appMatch = applicationModeMap.get(c.template_id); + return ({ id: c.template_id, name: c.template_id, score: c.confidence ?? 0, @@ -559,7 +572,15 @@ export async function loadRun(runId: string): Promise { decision: c.decision, reason: c.reason, capacityFit: c.capacity_fit, - })); + // ─── IMP-41 u2 — application_mode forwarding (issue #70) ─────────── + // Source = unit.application_candidates[] indexed by template_id above. + // Optional fields — undefined when no matching application_candidate + // (legacy fixtures pre-IMP-32 or candidates filtered out at Step 9). + applicationMode: appMatch?.application_mode, + autoApplicable: appMatch?.auto_applicable, + delegatedTo: appMatch?.delegated_to ?? null, + }); + }); const displayStrategy = ( runMeta.display_strategy_candidates_by_zone[posEntry.name]?.[0] ?? diff --git a/Front/client/src/types/designAgent.ts b/Front/client/src/types/designAgent.ts index 34fdda9..baf6831 100644 --- a/Front/client/src/types/designAgent.ts +++ b/Front/client/src/types/designAgent.ts @@ -175,6 +175,19 @@ export interface FrameCandidate { reason?: string | null; /** Capacity vs. content shape audit (compute_capacity_fit output). */ capacityFit?: CapacityFitEvidence | null; + + // ─── IMP-41 application_mode forwarding (issue #70 u1) ───────────────────── + // Source = src/phase_z2_pipeline.py APPLICATION_MODE_BY_V4_LABEL (:107-112), + // emitted by _application_candidates_for_unit() into Step 9 + // unit.application_candidates[]. Optional — legacy fixtures pre-IMP-32 omit + // these and the FramePanel tooltip falls back to the raw V4 label. + + /** Application mode mapped from V4 label by backend (authoritative). */ + applicationMode?: 'direct_insert' | 'same_frame_with_adjustment' | 'layout_or_region_change' | 'exclude'; + /** True when backend marks the candidate as automatically applicable. */ + autoApplicable?: boolean; + /** Delegation target step / actor (e.g. "step10_contract_check", "human_review"). */ + delegatedTo?: string | null; } // ───────────────────────────────────────────────────────────────────────────── diff --git a/Front/client/tests/imp41_application_mode.test.ts b/Front/client/tests/imp41_application_mode.test.ts new file mode 100644 index 0000000..96ec3b9 --- /dev/null +++ b/Front/client/tests/imp41_application_mode.test.ts @@ -0,0 +1,123 @@ +// IMP-41 u3 — Vitest coverage for application_mode helper (issue #70). +// +// Scope (Stage 2 unit u3 contract): +// 1) buildBadgeTitle: composite output for each known mode + legacy fallback +// (undefined applicationMode) + unknown fallback (string not in +// APPLICATION_MODE_TOOLTIP_KR). +// 2) mergeApplicationCandidates: array → Map +// semantics, including skip-missing-key and empty-input. +// +// Pure helper unit test — no React, no DOM, no fetch. Aligns with the +// AI-isolation contract: assertions key by backend application_mode VALUE, +// never by V4 label. + +import { describe, it, expect } from "vitest"; +import { + buildBadgeTitle, + mergeApplicationCandidates, + APPLICATION_MODE_TOOLTIP_KR, +} from "../src/services/applicationMode"; + +describe("buildBadgeTitle (IMP-41 u3)", () => { + it("returns composite ' ()' for direct_insert", () => { + expect(buildBadgeTitle("use_as_is", "direct_insert")).toBe( + `${APPLICATION_MODE_TOOLTIP_KR.direct_insert} (direct_insert)`, + ); + }); + + it("returns composite output for same_frame_with_adjustment", () => { + expect( + buildBadgeTitle("light_edit", "same_frame_with_adjustment"), + ).toBe( + `${APPLICATION_MODE_TOOLTIP_KR.same_frame_with_adjustment} (same_frame_with_adjustment)`, + ); + }); + + it("returns composite output for layout_or_region_change", () => { + expect( + buildBadgeTitle("restructure", "layout_or_region_change"), + ).toBe( + `${APPLICATION_MODE_TOOLTIP_KR.layout_or_region_change} (layout_or_region_change)`, + ); + }); + + it("returns composite output for exclude", () => { + expect(buildBadgeTitle("reject", "exclude")).toBe( + `${APPLICATION_MODE_TOOLTIP_KR.exclude} (exclude)`, + ); + }); + + it("falls back to 'V4 label: