feat(#73): IMP-44 u1~u5 layout override unknown-key guard + frontend zone_geometries validation
Some checks failed
Multi-MDX Regression (IMP-91) / multi-mdx-regression (push) Failing after 23s
Some checks failed
Multi-MDX Regression (IMP-91) / multi-mdx-regression (push) Failing after 23s
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -19,6 +19,7 @@ import {
|
||||
deriveUserOverridesKey,
|
||||
applyPersistedNonFrameOverrides,
|
||||
remapPersistedFramesToZoneFrames,
|
||||
validateZoneGeometriesAgainstLayout,
|
||||
} from "../utils/slidePlanUtils";
|
||||
import {
|
||||
parseMdxFile,
|
||||
@@ -154,6 +155,21 @@ export default function Home() {
|
||||
}
|
||||
carriedZoneSections[targetPos].push(...zone.section_ids);
|
||||
});
|
||||
// IMP-44 (#73) u4 — clear in-memory zone_geometries on layout flip.
|
||||
// The persisted keys were valid for the *prior* preset; carrying them
|
||||
// forward into the new preset would either trip the u1/u2 backend
|
||||
// [override-warning] guards (foreign keys dropped, override_applied
|
||||
// forced back to None) or partially apply on shared keys. Drop them
|
||||
// up-front so the new layout starts from a clean even-split baseline,
|
||||
// and persist a clear sentinel (null) so a subsequent reopen does not
|
||||
// resurrect the stale snapshot from user_overrides.json.
|
||||
const priorGeoms = p.userSelection.overrides.zone_geometries;
|
||||
const hadPriorGeoms =
|
||||
priorGeoms && typeof priorGeoms === "object" && Object.keys(priorGeoms).length > 0;
|
||||
if (p.uploadedFile && hadPriorGeoms) {
|
||||
const key = deriveUserOverridesKey(p.uploadedFile.name);
|
||||
void saveUserOverrides(key, { zone_geometries: null });
|
||||
}
|
||||
return {
|
||||
...p,
|
||||
userSelection: {
|
||||
@@ -162,6 +178,7 @@ export default function Home() {
|
||||
...p.userSelection.overrides,
|
||||
layout_preset: layoutId,
|
||||
zone_sections: carriedZoneSections,
|
||||
zone_geometries: {},
|
||||
},
|
||||
selectedZoneId: null,
|
||||
selectedRegionId: null,
|
||||
@@ -329,9 +346,28 @@ export default function Home() {
|
||||
|
||||
// zone-geometry override — backend 의 build_layout_css 에 전달 (horizontal-2 /
|
||||
// vertical-2 만 적용). zone_id (top/bottom/...) → slide-body 내부 0~1 비율.
|
||||
// IMP-44 (#73) u4 — validate against the active layout *before* the
|
||||
// round-trip so foreign-preset keys never reach the backend. Mirrors
|
||||
// the u1/u2 WARN+DROP guards on the frontend side: dropped keys surface
|
||||
// as a toast (so the user knows why their resize "vanished"), and only
|
||||
// the `kept` subset is forwarded. The active layout = the layout the
|
||||
// backend will use, which is `overrides.layout` when the user has set
|
||||
// one, else the default slidePlan preset (mirrors backend resolution).
|
||||
const zoneGeometries = state.userSelection.overrides.zone_geometries;
|
||||
if (zoneGeometries && Object.keys(zoneGeometries).length > 0) {
|
||||
overrides.zoneGeometries = zoneGeometries;
|
||||
const activeLayout = overrides.layout ?? sourcePlan.layout_preset;
|
||||
const validation = validateZoneGeometriesAgainstLayout(
|
||||
zoneGeometries,
|
||||
activeLayout,
|
||||
);
|
||||
if (Object.keys(validation.dropped).length > 0) {
|
||||
toast.error(
|
||||
`zone_geometries layout-mismatch: dropped ${Object.keys(validation.dropped).join(", ")} (expected ${validation.expectedPositions.join(", ") || "—"}; layout=${activeLayout}).`,
|
||||
);
|
||||
}
|
||||
if (Object.keys(validation.kept).length > 0) {
|
||||
overrides.zoneGeometries = validation.kept;
|
||||
}
|
||||
}
|
||||
|
||||
// 2026-05-22 — IMP-08 B-3 원래 동작 (sameAsDefault with effectiveSlidePlan) 복귀.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { UserSelection, SlidePlan, Zone, InternalRegion, LayoutPresetId } from "../types/designAgent";
|
||||
import type { UserOverrides } from "../services/userOverridesApi";
|
||||
import { computeZonePositions } from "../services/designAgentApi";
|
||||
|
||||
// ─── IMP-52 u6 — restore-on-reopen helpers (pure, exported for testing) ────
|
||||
// These helpers compose persisted `user_overrides.json` payloads (typed by
|
||||
@@ -320,3 +321,77 @@ export function getEffectiveLayoutId(slidePlan: SlidePlan | null, selection: Use
|
||||
if (selection.overrides.layout_preset) return selection.overrides.layout_preset;
|
||||
return slidePlan?.layout_preset || 'single';
|
||||
}
|
||||
|
||||
// ─── IMP-44 (#73) u3 — zone_geometries layout-mismatch validation ───────────
|
||||
// Pure helper paired with the backend [override-warning] guards added in u1
|
||||
// (1-D horizontal-2 / vertical-2 branches of `build_layout_css`) and u2 (2-D
|
||||
// `_override_to_grid_tracks` call site). Same WARN+DROP / KEEP-known contract,
|
||||
// but expressed on the frontend so handleGenerate (u4) can validate against
|
||||
// the active layout *before* forwarding and surface a toast on dropped keys.
|
||||
//
|
||||
// Source of truth for expected positions = `computeZonePositions(layoutPreset)`
|
||||
// (designAgentApi.ts), which mirrors backend `layouts.yaml` (positions field).
|
||||
// Unknown layout (null / undefined / not in LAYOUT_PRESET_IDS) ⇒ fail-safe
|
||||
// drop-all: caller has no contract for projecting geometries onto an unknown
|
||||
// preset, so we keep zero keys rather than passing them through verbatim.
|
||||
|
||||
export interface ZoneGeometryValue {
|
||||
x: number;
|
||||
y: number;
|
||||
w: number;
|
||||
h: number;
|
||||
}
|
||||
|
||||
export interface ZoneGeometriesValidationResult {
|
||||
kept: Record<string, ZoneGeometryValue>;
|
||||
dropped: Record<string, ZoneGeometryValue>;
|
||||
expectedPositions: string[];
|
||||
valid: boolean;
|
||||
}
|
||||
|
||||
export function validateZoneGeometriesAgainstLayout(
|
||||
geoms: Record<string, ZoneGeometryValue> | null | undefined,
|
||||
layoutPreset: LayoutPresetId | string | null | undefined,
|
||||
): ZoneGeometriesValidationResult {
|
||||
const kept: Record<string, ZoneGeometryValue> = {};
|
||||
const dropped: Record<string, ZoneGeometryValue> = {};
|
||||
const safeGeoms =
|
||||
geoms && typeof geoms === "object" && !Array.isArray(geoms) ? geoms : null;
|
||||
|
||||
// Unknown-layout fail-safe — drop everything; no expected positions known.
|
||||
if (typeof layoutPreset !== "string" || !LAYOUT_PRESET_IDS.has(layoutPreset)) {
|
||||
if (safeGeoms) {
|
||||
for (const [k, v] of Object.entries(safeGeoms)) {
|
||||
dropped[k] = v;
|
||||
}
|
||||
}
|
||||
return {
|
||||
kept,
|
||||
dropped,
|
||||
expectedPositions: [],
|
||||
valid: Object.keys(dropped).length === 0,
|
||||
};
|
||||
}
|
||||
|
||||
const expectedPositions = computeZonePositions(
|
||||
layoutPreset as LayoutPresetId,
|
||||
).map((p) => p.name);
|
||||
const expectedSet = new Set(expectedPositions);
|
||||
|
||||
if (safeGeoms) {
|
||||
for (const [k, v] of Object.entries(safeGeoms)) {
|
||||
if (expectedSet.has(k)) {
|
||||
kept[k] = v;
|
||||
} else {
|
||||
dropped[k] = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
kept,
|
||||
dropped,
|
||||
expectedPositions,
|
||||
valid: Object.keys(dropped).length === 0,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user