fix(dupont): reject out-of-domain window + bidirectional clip + suffix discipline (R1-R4/M2)
ci/woodpecker/push/main Pipeline failed

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.
This commit is contained in:
wecom-fullstack
2026-10-05 02:02:21 +08:00
parent 3ed9fa0611
commit 4f9bab6bc0
4 changed files with 278 additions and 21 deletions
+4 -4
View File
@@ -23,7 +23,7 @@ from app.utils.deviation_engine import calc_period_deviation, calc_period_diff,
from app.utils.alert_entity_scope import month_period_clause
from app.utils.profitability_params import load_profitability_params
from app.utils.dupont_basis import (
annualize_factors, available_months, build_window_meta, check_identity,
annualize_factors, apply_period_suffix, available_months, build_window_meta, check_identity,
compute_factors, latest_stock_month, load_dupont_params, months_between, prev_month,
resolve_window, stock_balance, sum_flows,
)
@@ -2330,9 +2330,9 @@ def get_dupont_analysis(
name_row = db.execute(
text("SELECT name FROM entities WHERE id = :eid"), {"eid": entity_id}).first()
entity_name = (name_row[0] if name_row else None) or ("陕西酣客文化传媒" if slug == "hanke" else "陕西博海网络科技")
suffix = {"ytd": "(YTD 年初至今)", "ttm": "(TTM 滚动 12 月)", "month": "(单月)",
"quarter": "(季度)", "half": "(半年)", "year": "(全年)", "custom": "(自定义区间)"}
label = win["label"] + suffix.get(win["period_key"], "")
# R3 后缀纪律:仅在「返回窗口 == 名义窗口」(未裁剪)时保留「(季度)/(半年)…」后缀;
# 发生任何裁剪 ⇒ 去后缀,period = 实际窗口标签(判据由 resolve_window 回传 `clipped`)。
label = apply_period_suffix(win["label"], win["period_key"], bool(win.get("clipped")))
cur = _dupont_window_values(db, entity_id, caliber, win["period_start"], win["period_end"])
ann = annualize_factors(dict(cur["factors"]), win["months"], annualize)