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: