diff --git a/Front/client/src/components/FramePanel.tsx b/Front/client/src/components/FramePanel.tsx index be4ad24..c7f383a 100644 --- a/Front/client/src/components/FramePanel.tsx +++ b/Front/client/src/components/FramePanel.tsx @@ -82,11 +82,67 @@ export default function FramePanel({ ) : ( candidates.map((candidate, index) => { const isSelected = currentFrameId === candidate.id; - + const isReject = candidate.label === "reject"; // catalog 미등록 = backend Step 7-A 가 override 시도해도 skip. // catalogRegistered === false 만 체크 (undefined = 정보 없음, 일반 처리). const isCatalogMissing = candidate.catalogRegistered === false; + + // ─── IMP-29 u3 — IMP-05 L2 candidate_evidence surface ─────────── + // All evidence fields optional; silent degradation when undefined + // (pre-IMP-05 fixtures fall back to label/catalogRegistered only). + const isFilteredDirect = candidate.filteredForDirectExecution === true; + const hasDecision = candidate.decision === "selected" || candidate.decision === "skipped"; + const isSkipped = candidate.decision === "skipped"; + const isSelectedDecision = candidate.decision === "selected"; + const showRouteChip = + candidate.routeHint && candidate.routeHint !== "direct_render"; + const showStatusChip = + candidate.phaseZStatus && candidate.phaseZStatus !== "auto_renderable"; + const hasCapacityFit = + candidate.capacityFit && candidate.capacityFit.fit_status; + const capacityMismatch = + hasCapacityFit && candidate.capacityFit!.fit_status !== "ok"; + + // Compose evidence tooltip lines (only when at least one signal present). + const evidenceLines: string[] = []; + if (candidate.decision) evidenceLines.push(`decision: ${candidate.decision}`); + if (candidate.reason) evidenceLines.push(`reason: ${candidate.reason}`); + if (candidate.routeHint) evidenceLines.push(`route: ${candidate.routeHint}`); + if (candidate.phaseZStatus) + evidenceLines.push(`phase_z_status: ${candidate.phaseZStatus}`); + if (hasCapacityFit) { + const cf = candidate.capacityFit!; + const capacityLine = + cf.fit_status === "ok" + ? `capacity: ok${ + typeof cf.item_count === "number" + ? ` (items=${cf.item_count})` + : "" + }` + : `capacity: ${cf.fit_status}${ + cf.mismatch_reason ? ` — ${cf.mismatch_reason}` : "" + }`; + evidenceLines.push(capacityLine); + } + const evidenceTooltip = + evidenceLines.length > 0 ? evidenceLines.join("\n") : undefined; + + // Compose final tooltip: existing catalog/reject reasons first, then + // evidence detail (preserves Phase Q tooltip semantics). + const tooltipParts = [ + isCatalogMissing + ? "⚠ catalog 미등록 — render path 에서 적용 안 됨 (선택해도 backend 가 skip)" + : null, + isFilteredDirect + ? "⚠ filtered_for_direct_execution — MVP1 직접 렌더 경로 제외" + : null, + isReject ? "V4 reject — render path 비추천" : null, + evidenceTooltip, + ].filter((s): s is string => Boolean(s)); + const composedTitle = + tooltipParts.length > 0 ? tooltipParts.join("\n\n") : undefined; + return ( {/* Rank Badge */}
@@ -199,6 +251,64 @@ export default function FramePanel({ {candidate.label} )} + {/* IMP-29 u3 — route hint chip (skip when direct_render = default). */} + {showRouteChip && ( + + {candidate.routeHint === "deterministic_minor_adjustment" + ? "adapt" + : candidate.routeHint === "ai_adaptation_required" + ? "ai req" + : candidate.routeHint === "design_reference_only" + ? "ref" + : candidate.routeHint} + + )} + {/* IMP-29 u3 — phase_z status warning chip (skip when auto_renderable). */} + {showStatusChip && ( + + {candidate.phaseZStatus!.replace(/_/g, " ")} + + )} + {/* IMP-29 u3 — capacity_fit indicator (ok = subtle, mismatch = warning). */} + {hasCapacityFit && ( + + {capacityMismatch + ? `fit: ${candidate.capacityFit!.fit_status}` + : "fit ok"} + + )} + {/* IMP-29 u3 — decision badge (Stage 2 contract: surface both selected & skipped). */} + {hasDecision && ( + + {isSkipped ? "skip" : "sel"} + + )} {isSelected && (
diff --git a/Front/client/src/services/designAgentApi.ts b/Front/client/src/services/designAgentApi.ts index b2ec5d0..c507f91 100644 --- a/Front/client/src/services/designAgentApi.ts +++ b/Front/client/src/services/designAgentApi.ts @@ -503,9 +503,20 @@ export async function loadRun(runId: string): Promise { restructure: 2, reject: 3, }; - const rawSource = (unit.v4_all_judgments?.length > 0) - ? unit.v4_all_judgments - : (unit.v4_candidates ?? []); + // IMP-29 u2 — source priority (deterministic, no LLM): + // 1) unit.candidate_evidence (IMP-05 L2 canonical, 14 fields per entry) + // 2) unit.v4_all_judgments (pre-IMP-05 audit array) + // 3) unit.v4_candidates (legacy minimal) + // fallback_chain alias is intentionally NOT read (Stage 2 guardrail). + const candidateEvidence = Array.isArray(unit.candidate_evidence) + ? unit.candidate_evidence + : []; + const rawSource = + candidateEvidence.length > 0 + ? candidateEvidence + : (unit.v4_all_judgments?.length > 0 + ? unit.v4_all_judgments + : (unit.v4_candidates ?? [])); const v4Source = [...rawSource].sort((a: any, b: any) => { const lp = (LABEL_PRIORITY[a.label] ?? 99) - (LABEL_PRIORITY[b.label] ?? 99); if (lp !== 0) return lp; @@ -525,12 +536,24 @@ export async function loadRun(runId: string): Promise { ? `/frame-preview/${String(c.frame_number).padStart(2, "0")}` : undefined, // backend step09 의 catalog_registered (frame_contracts.yaml 등록 여부). - // v4_all_judgments 에만 있음. v4_candidates fallback 시 undefined. + // candidate_evidence 및 v4_all_judgments 에 있음. v4_candidates fallback 시 undefined. catalogRegistered: c.catalog_registered, // backend step09 의 min_height_px (frame_contracts.yaml visual_hints.min_height_px). // logical 1280x720 px 좌표계. contract 미등록 또는 visual_hints 부재 시 undefined. - // v4_all_judgments 에만 있음. v4_candidates fallback 시 undefined (graceful). + // v4_all_judgments 에만 있음. candidate_evidence / v4_candidates fallback 시 undefined (graceful). minHeightPx: c.min_height_px ?? undefined, + // ─── IMP-05 L2 candidate_evidence fields (IMP-29 u2) ───────────────── + // Populated when source = unit.candidate_evidence; otherwise silently + // undefined for legacy fixtures (pre-IMP-05 fallback path). + rank: c.rank, + frameId: c.frame_id, + v4Label: c.v4_label, + phaseZStatus: c.phase_z_status, + filteredForDirectExecution: c.filtered_for_direct_execution, + routeHint: c.route_hint, + decision: c.decision, + reason: c.reason, + capacityFit: c.capacity_fit, })); const displayStrategy = ( diff --git a/Front/client/src/types/designAgent.ts b/Front/client/src/types/designAgent.ts index 5ca099f..34fdda9 100644 --- a/Front/client/src/types/designAgent.ts +++ b/Front/client/src/types/designAgent.ts @@ -116,6 +116,23 @@ export interface InternalRegion { frame_candidates: FrameCandidate[]; } +/** IMP-05 L2 candidate_evidence.capacity_fit — backend capacity vs. content shape audit. + * Source = src/phase_z2_pipeline.py compute_capacity_fit(). All fields optional — + * frontend tolerates absence for pre-IMP-05 fixtures and contract-less templates. */ +export interface CapacityFitEvidence { + item_count?: number | null; + source_shape?: string | null; + capacity?: { + strict?: number | null; + min?: number | null; + max?: number | null; + truncate_at?: number | null; + pad_to?: number | null; + } | null; + fit_status?: string | null; + mismatch_reason?: string | null; +} + /** 프레임 후보 (V4 매칭 결과) */ export interface FrameCandidate { id: string; @@ -131,6 +148,33 @@ export interface FrameCandidate { * Source = templates/phase_z2/catalog/frame_contracts.yaml visual_hints.min_height_px. * Undefined when contract unregistered or visual_hints absent (frontend tolerates undefined). */ minHeightPx?: number; + + // ─── IMP-05 L2 candidate_evidence fields (IMP-29 u1) ─────────────────────── + // Source = src/phase_z2_pipeline.py lookup_v4_match_with_fallback() candidate_trace. + // All fields optional — pre-IMP-05 fixtures fall back to v4_all_judgments/v4_candidates + // (deterministic, no LLM) and silently leave these undefined. + + /** Candidate rank in V4 chain (1-based; 1 = primary). */ + rank?: number; + /** Figma frame node id (backend `frame_id`). Distinct from `id` (= template_id). */ + frameId?: string; + /** Alias of `label`. Kept separate for Codex IMP-05 L2 schema parity. */ + v4Label?: 'use_as_is' | 'light_edit' | 'restructure' | 'reject'; + /** Phase Z status enum (e.g. "auto_renderable", "fallback_candidate"). Open vocabulary. */ + phaseZStatus?: string; + /** True when status is outside MVP1_ALLOWED_STATUSES (= excluded from direct render path). */ + filteredForDirectExecution?: boolean; + /** Execution route mapped from `label` (direct_render / deterministic_minor_adjustment / + * ai_adaptation_required / design_reference_only). Null on unknown labels. */ + routeHint?: 'direct_render' | 'deterministic_minor_adjustment' | 'ai_adaptation_required' | 'design_reference_only' | null; + /** Selection outcome ("selected" or "skipped"). */ + decision?: 'selected' | 'skipped'; + /** Human-readable rationale (e.g. "primary_selected", "fallback_selected", + * "duplicate_template_id", "skipped_no_contract", "capacity_mismatch:...", + * "phase_z_status_not_allowed:..."). */ + reason?: string | null; + /** Capacity vs. content shape audit (compute_capacity_fit output). */ + capacityFit?: CapacityFitEvidence | null; } // ─────────────────────────────────────────────────────────────────────────────