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

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-24 12:12:24 +09:00
parent 5deeb97cf6
commit e0c39f1bc1
5 changed files with 565 additions and 70 deletions

View File

@@ -1923,83 +1923,139 @@ def build_layout_css(layout_preset: str, zones_data: list[dict],
# ── Step D-ext : user override 처리 ──
if override_zone_geometries:
if layout_preset == "horizontal-2":
# heights_px override — zone 의 h 비율로 SLIDE_BODY_HEIGHT 분배.
# Hot-fix (2026-05-22): partial override = 나머지 공간을 비-override zone 들에
# 균등 분배 (drag boundary intent). 이전엔 0.0 fallback → 100/0 깨짐.
overridden_h = sum(
float(override_zone_geometries[p]["h"])
for p in positions if p in override_zone_geometries
# IMP-44 u1 — unknown-key guard: drop foreign-preset keys
# (예: vertical-2 keys {left,right} sent to horizontal-2), emit
# structured warning, keep matching keys. All-unknown → fall
# through to default dynamic dispatch (no false override_applied).
unknown_keys = sorted(
k for k in override_zone_geometries if k not in positions
)
non_overridden = [p for p in positions if p not in override_zone_geometries]
per_non = max(0.0, 1.0 - overridden_h) / max(len(non_overridden), 1)
ratios = []
for pos in positions:
geom = override_zone_geometries.get(pos)
ratios.append(float(geom["h"]) if geom else per_non)
total = sum(ratios)
if total > 0:
heights_px = [int(round(r / total * SLIDE_BODY_HEIGHT)) for r in ratios]
rows = " ".join(f"{h}px" for h in heights_px)
return {
"areas": preset["css_areas"],
"cols": preset["css_cols"],
"rows": rows,
"heights_px": heights_px,
"widths_px": [SLIDE_BODY_WIDTH],
"ratios": [round(r / total, 3) for r in ratios],
"width_ratios": [1.0],
"computation": "user_override_geometry",
"dynamic_rows": True,
"dynamic_cols": False,
"raw_zone_layout": {"override_applied": True, "source": override_zone_geometries},
}
if unknown_keys:
print(
f" [override-warning] layout_preset={layout_preset} "
f"expected_positions={list(positions)} unknown_keys={unknown_keys} "
f"(dropped foreign-preset keys; default split for non-overridden).",
file=sys.stderr,
)
filtered_overrides = {
k: v for k, v in override_zone_geometries.items() if k in positions
}
if filtered_overrides:
# heights_px override — zone 의 h 비율로 SLIDE_BODY_HEIGHT 분배.
# Hot-fix (2026-05-22): partial override = 나머지 공간을 비-override zone 들에
# 균등 분배 (drag boundary intent). 이전엔 0.0 fallback → 100/0 깨짐.
overridden_h = sum(
float(filtered_overrides[p]["h"])
for p in positions if p in filtered_overrides
)
non_overridden = [p for p in positions if p not in filtered_overrides]
per_non = max(0.0, 1.0 - overridden_h) / max(len(non_overridden), 1)
ratios = []
for pos in positions:
geom = filtered_overrides.get(pos)
ratios.append(float(geom["h"]) if geom else per_non)
total = sum(ratios)
if total > 0:
heights_px = [int(round(r / total * SLIDE_BODY_HEIGHT)) for r in ratios]
rows = " ".join(f"{h}px" for h in heights_px)
return {
"areas": preset["css_areas"],
"cols": preset["css_cols"],
"rows": rows,
"heights_px": heights_px,
"widths_px": [SLIDE_BODY_WIDTH],
"ratios": [round(r / total, 3) for r in ratios],
"width_ratios": [1.0],
"computation": "user_override_geometry",
"dynamic_rows": True,
"dynamic_cols": False,
"raw_zone_layout": {"override_applied": True, "source": filtered_overrides},
}
elif layout_preset == "vertical-2":
# cols override — zone 의 w 비율로 fr 분배 (legacy: fr-string cols).
# PR 1 keeps fr-string cols for legacy preserve; widths_px is
# populated in pixels for _compute_per_zone_geometry length contract.
# Hot-fix (2026-05-22): partial override = 나머지 공간을 비-override zone 들에
# 균등 분배 (drag boundary intent). 이전엔 0.0 fallback → 100/0 깨짐.
overridden_w = sum(
float(override_zone_geometries[p]["w"])
for p in positions if p in override_zone_geometries
# IMP-44 u1 — unknown-key guard: drop foreign-preset keys
# (예: horizontal-2 keys {top,bottom} sent to vertical-2), emit
# structured warning, keep matching keys. All-unknown → fall
# through to default dynamic dispatch (no false override_applied).
unknown_keys = sorted(
k for k in override_zone_geometries if k not in positions
)
non_overridden = [p for p in positions if p not in override_zone_geometries]
per_non = max(0.0, 1.0 - overridden_w) / max(len(non_overridden), 1)
ratios = []
for pos in positions:
geom = override_zone_geometries.get(pos)
ratios.append(float(geom["w"]) if geom else per_non)
total = sum(ratios)
if total > 0:
cols = " ".join(f"{round(r / total * 100, 2)}fr" for r in ratios)
normalized = [r / total for r in ratios]
widths_px = [
int(round(rr * (SLIDE_BODY_WIDTH - gap * (len(ratios) - 1))))
for rr in normalized
]
diff = (SLIDE_BODY_WIDTH - gap * (len(ratios) - 1)) - sum(widths_px)
if diff != 0 and widths_px:
widths_px[-1] += diff
return {
"areas": preset["css_areas"],
"cols": cols,
"rows": preset["css_rows"],
"heights_px": [SLIDE_BODY_HEIGHT],
"widths_px": widths_px,
"ratios": [1.0],
"width_ratios": [round(rr, 3) for rr in normalized],
"computation": "user_override_geometry",
"dynamic_rows": False,
"dynamic_cols": True,
"raw_zone_layout": {"override_applied": True, "source": override_zone_geometries},
}
if unknown_keys:
print(
f" [override-warning] layout_preset={layout_preset} "
f"expected_positions={list(positions)} unknown_keys={unknown_keys} "
f"(dropped foreign-preset keys; default split for non-overridden).",
file=sys.stderr,
)
filtered_overrides = {
k: v for k, v in override_zone_geometries.items() if k in positions
}
if filtered_overrides:
# cols override — zone 의 w 비율로 fr 분배 (legacy: fr-string cols).
# PR 1 keeps fr-string cols for legacy preserve; widths_px is
# populated in pixels for _compute_per_zone_geometry length contract.
# Hot-fix (2026-05-22): partial override = 나머지 공간을 비-override zone 들에
# 균등 분배 (drag boundary intent). 이전엔 0.0 fallback → 100/0 깨짐.
overridden_w = sum(
float(filtered_overrides[p]["w"])
for p in positions if p in filtered_overrides
)
non_overridden = [p for p in positions if p not in filtered_overrides]
per_non = max(0.0, 1.0 - overridden_w) / max(len(non_overridden), 1)
ratios = []
for pos in positions:
geom = filtered_overrides.get(pos)
ratios.append(float(geom["w"]) if geom else per_non)
total = sum(ratios)
if total > 0:
cols = " ".join(f"{round(r / total * 100, 2)}fr" for r in ratios)
normalized = [r / total for r in ratios]
widths_px = [
int(round(rr * (SLIDE_BODY_WIDTH - gap * (len(ratios) - 1))))
for rr in normalized
]
diff = (SLIDE_BODY_WIDTH - gap * (len(ratios) - 1)) - sum(widths_px)
if diff != 0 and widths_px:
widths_px[-1] += diff
return {
"areas": preset["css_areas"],
"cols": cols,
"rows": preset["css_rows"],
"heights_px": [SLIDE_BODY_HEIGHT],
"widths_px": widths_px,
"ratios": [1.0],
"width_ratios": [round(rr, 3) for rr in normalized],
"computation": "user_override_geometry",
"dynamic_rows": False,
"dynamic_cols": True,
"raw_zone_layout": {"override_applied": True, "source": filtered_overrides},
}
elif topology in ("T", "inverted-T", "side-T-left", "side-T-right", "2x2"):
# IMP-09 PR 2 — 2-D override path (T / inverted-T / side-T / 2x2).
# Degenerate inputs (total_h == 0 or total_w == 0) fall back to
# _build_grid_dynamic_2d inside the helper.
return _override_to_grid_tracks(
preset, zones_data, override_zone_geometries, gap=gap
#
# IMP-44 u2 — unknown-key guard mirrors u1 (1-D): drop foreign-
# preset keys (예: vertical-2 keys {left,right} sent to T-preset),
# emit structured warning, keep matching keys. All-unknown → fall
# through to _build_grid_dynamic_2d default (no false override_applied).
unknown_keys = sorted(
k for k in override_zone_geometries if k not in positions
)
if unknown_keys:
print(
f" [override-warning] layout_preset={layout_preset} "
f"expected_positions={list(positions)} unknown_keys={unknown_keys} "
f"(dropped foreign-preset keys; default split for non-overridden).",
file=sys.stderr,
)
filtered_overrides = {
k: v for k, v in override_zone_geometries.items() if k in positions
}
if filtered_overrides:
return _override_to_grid_tracks(
preset, zones_data, filtered_overrides, gap=gap
)
return _build_grid_dynamic_2d(preset, zones_data, gap=gap)
else:
# warn-and-fallthrough preserved for remaining presets (single).
# PR 3 territory.