From ab2764c8d078791dfb8a4b8930883f07b42f93e2 Mon Sep 17 00:00:00 2001 From: kyeongmin Date: Fri, 15 May 2026 22:36:16 +0900 Subject: [PATCH] =?UTF-8?q?feat(IMP-08):=20U3=20=E2=80=94=20frontend=20wir?= =?UTF-8?q?e=20(zoneSections=20override)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wires the frontend drag/drop zone assignment through to the backend --override-section-assignment CLI flag. PipelineOverrides gains an optional zoneSections field (Record) carrying canonical ordinal section ids (e.g., "top": ["04-2-sub-1"]). Vite middleware /api/run accepts overrides.zoneSections and forwards each non-empty zone as `--override-section-assignment ZONE=sid[,sid]`. Empty arrays and non-string sids are filtered to avoid bogus assignments from a partially-built UI state. Home.tsx builds the override with a diff-vs-default guard per Codex Stage 3 R3 B3 fix : createInitialUserSelection seeds zone_sections with the auto plan, so a literal copy would pollute backend assignment-source provenance even on a fresh re-render. The diff compares each zone's section list against sourcePlan.zones[].section_ids and only emits zones that differ. Toast summary now reports zoneSections=N when forwarded. Smoke verification : python -m src.phase_z2_pipeline samples/mdx_batch/04.mdx test_imp08_smoke --override-section-assignment primary=04-2-sub-1 produces section_assignment_plan with assignment_source=cli_override and v4_selector_trace.candidates populated via the U1 alias resolver (04-2-sub-1 -> 04-2.1 V4 entry). Co-Authored-By: Claude Opus 4.7 (1M context) --- Front/client/src/pages/Home.tsx | 31 +++++++++++++++++++++ Front/client/src/services/designAgentApi.ts | 5 ++++ Front/vite.config.ts | 18 ++++++++++++ 3 files changed, 54 insertions(+) diff --git a/Front/client/src/pages/Home.tsx b/Front/client/src/pages/Home.tsx index 04adf53..da29b96 100644 --- a/Front/client/src/pages/Home.tsx +++ b/Front/client/src/pages/Home.tsx @@ -300,6 +300,35 @@ export default function Home() { if (zoneGeometries && Object.keys(zoneGeometries).length > 0) { overrides.zoneGeometries = zoneGeometries; } + + // IMP-08 B-3 : zoneSections forward only when the user diverged from + // the auto plan. Codex Stage 3 R3 B3 fix : `createInitialUserSelection` + // seeds `zone_sections` with the default placement, so a literal copy + // would pollute backend assignment-source provenance even on a fresh + // re-render. Diff against `sourcePlan.zones[].section_ids` per zone and + // only emit zones whose section list differs. + const userZoneSections = state.userSelection.overrides.zone_sections; + if (userZoneSections) { + const defaultByZone = new Map(); + sourcePlan.zones.forEach((z) => { + defaultByZone.set(z.zone_id, z.section_ids); + }); + const zoneSectionsDiff: Record = {}; + for (const [zoneId, sids] of Object.entries(userZoneSections)) { + if (!Array.isArray(sids)) continue; + const cleaned = sids.filter((s) => typeof s === "string" && s.trim()); + const defaults = defaultByZone.get(zoneId) ?? []; + const sameAsDefault = + cleaned.length === defaults.length && + cleaned.every((sid, i) => sid === defaults[i]); + if (!sameAsDefault) { + zoneSectionsDiff[zoneId] = cleaned; + } + } + if (Object.keys(zoneSectionsDiff).length > 0) { + overrides.zoneSections = zoneSectionsDiff; + } + } } setState((p) => ({ ...p, isLoading: true })); @@ -310,6 +339,8 @@ export default function Home() { ? `(overrides: ${[ overrides.layout && `layout=${overrides.layout}`, overrides.frames && `frames=${Object.keys(overrides.frames).length}`, + overrides.zoneSections && + `zoneSections=${Object.keys(overrides.zoneSections).length}`, ] .filter(Boolean) .join(", ")})` diff --git a/Front/client/src/services/designAgentApi.ts b/Front/client/src/services/designAgentApi.ts index 8d8fb2d..accb166 100644 --- a/Front/client/src/services/designAgentApi.ts +++ b/Front/client/src/services/designAgentApi.ts @@ -251,6 +251,11 @@ export interface PipelineOverrides { /** zone_id (top/bottom/left/right/...) → slide-body 내부 0~1 비율. * backend 의 build_layout_css 가 horizontal-2 / vertical-2 만 처리. */ zoneGeometries?: Record; + /** IMP-08 B-3 : zone_id -> list of section_id assignments + * (canonical ordinal `${parent}-sub-${n}`). Only forwarded when the + * user explicitly diverges from the auto plan; default placements + * are not echoed back to avoid polluting override provenance. */ + zoneSections?: Record; } export async function runPipeline( diff --git a/Front/vite.config.ts b/Front/vite.config.ts index d3d151a..789d708 100644 --- a/Front/vite.config.ts +++ b/Front/vite.config.ts @@ -241,6 +241,9 @@ function vitePluginPhaseZApi(): Plugin { layout?: string; frames?: Record; // unit_id → template_id zoneGeometries?: Record; // zone_id → bbox (slide-body 내부 0~1) + // IMP-08 B-3 : zone_id -> list of canonical section_id assignments + // (e.g., "top": ["03-1-sub-1"]). Forwarded as --override-section-assignment. + zoneSections?: Record; }; }; try { @@ -322,6 +325,21 @@ function vitePluginPhaseZApi(): Plugin { } } } + // IMP-08 B-3 — zoneSections override forward to CLI. + // Each entry becomes `--override-section-assignment ZONE=sid[,sid]`. + // Empty arrays and non-string sids are filtered out so the backend + // never receives bogus assignments from a partially-built UI state. + if (overrides?.zoneSections && typeof overrides.zoneSections === "object") { + for (const [zoneId, sids] of Object.entries(overrides.zoneSections)) { + if (!Array.isArray(sids)) continue; + const cleaned = sids.filter((s) => typeof s === "string" && s.trim()); + if (cleaned.length === 0) continue; + cliArgs.push( + "--override-section-assignment", + `${zoneId}=${cleaned.join(",")}` + ); + } + } console.log( `[phase-z-api] spawn pipeline: run_id=${runId}, mdx=${mdxPath}, args=${JSON.stringify(cliArgs.slice(2))}` );