Per finance ruling 2026-10-05 (project-20261005-dupont-oob-window-fix):
- R1: no intersection with available month domain => explicit error (endpoint 400), no window fields
- R2: with intersection => intersection window, months>=1 (kills inversion and months<=0)
- R3: quarter/half/... suffix kept only when returned window == nominal window (clipped flag)
- R4: symmetric lower-bound clip note; M2: TTM nominal window = [anchor-11, anchor] (12 months)
- M1: docstring correction (lower-bound overflow is clipped, not 400)
No push / no deploy / no restart / no DB write.
Root cause: resolve_window() matched quarter/half with p[4] == "q" / "h"
(always '-' for "2026-q2"/"2026-h1"), and the branches sat after the
single-month branch -> dead code. Real input fell into the month branch,
so months_between() raised ValueError on int('q2') -> HTTP 500 for all
18 quarter/half options in the period selector.
Fix: judge on p[5] in ("q","h") with a digit guard on p[6], evaluated
before the single-month branch; month branch tightened to p[5:].isdigit().
Existing error-branch wording unchanged.
Tests: backend/tests/test_dupont_period_options.py (18 defect options,
case equivalence, month/year/ytd/ttm/custom non-regression, malformed
input returns error dict instead of raising).