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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user