fix(alerts): enforce caliber ruling R5-supp (period linkage)
deviation_engine: link kpi_value_id on deviation/trend alert rows; skip when period not attributable or kpi_values row missing; dedupe key now (entity_id,kpi_id,kpi_value_id,alert_type) deviation_push: same-source R1/R5 guards, no-period request writes nothing tests: add test_alert_caliber_r5_residual_20260919 (13 cases incl. negative controls); adapt r1r4/r5 fixtures to the supplemental ruling
This commit is contained in:
@@ -15,10 +15,15 @@
|
||||
类A(可落地守卫,本套件 RED→GREEN 对象):期间已知、可能为伪期间的生成路径
|
||||
—— 规则引擎(`latest_value.period` 按字符串 desc 取,`2026-H1` 会排在 `2026-09` 之前)、
|
||||
Bot 写通道(`bot_bridge` / `bot_bridge_v2` 按显式 period 写)。
|
||||
类B(现状登记,见 `TestR5ClassBStatusQuo`):设计上不携带月度期间的族(差异/趋势/集中度/
|
||||
到期未收款/资金缺口预测),其期间**无从从库派生**(`kpi_value_id IS NULL`);
|
||||
裁定边界原文「若日后确有『无期间合法预警』的业务需求,由产品侧另提,本裁定不预设」
|
||||
→ 本单登记为待产品侧决策项,不做静默屏蔽(会灭掉既有族并触发既有套件失败)。
|
||||
类B(现状登记,见 `TestR5ClassBStatusQuo`):设计上不携带月度期间的族(集中度/到期未收款/
|
||||
资金缺口预测),其期间**无从从库派生**(`kpi_value_id IS NULL`)。
|
||||
|
||||
2026-09-19 补充裁定 R5-补(财务,收口两个残口)后本套件的口径变化:
|
||||
- **补1**:差异/趋势/差异反打路径改为**必须落 `kpi_value_id`**(不可归期即跳过),
|
||||
差异类去重键改 `(entity_id, kpi_id, kpi_value_id, alert_type)` → **差异/趋势已由类B 转入类A**,
|
||||
其守卫与负对照用例落在 `tests/test_alert_caliber_r5_residual_20260919.py`;
|
||||
- **补2**:类B「无月度期间族」**不屏蔽(现状保留)**,登记为已知例外,不再作为待决策项挂账
|
||||
→ `TestR5ClassBStatusQuo` 由「待产品侧决策」的登记,改为「按补2 保留」的登记。
|
||||
"""
|
||||
from datetime import datetime
|
||||
|
||||
@@ -217,31 +222,18 @@ class TestR5BotWritePaths:
|
||||
class TestR5ClassBStatusQuo:
|
||||
"""类B 现状登记(非守卫断言)——1b 实测证据:这些生成路径产出的行**期间无从从库派生**。
|
||||
|
||||
裁定边界原文:「若日后确有『无期间合法预警』的业务需求,由产品侧另提,本裁定不预设」。
|
||||
本单结论:按边界**不做静默屏蔽**(屏蔽会灭掉 423 行既有族 + 触发既有套件失败),
|
||||
登记为待产品侧决策项。若产品侧裁定需屏蔽,本用例应随之更新。
|
||||
裁定演进(2026-09-19 §补充裁定 R5-补2):类B「无月度期间族」**不屏蔽(现状保留)**,
|
||||
理由 = 驱动量是日期/占比/趋势,可行动性不以月度期间为前提(属 R5 边界条款所指
|
||||
「无期间合法预警」业务需求);屏蔽会一次性灭掉「到期未收款」等既有能力(存量 423 行族)
|
||||
并触发既有套件失败 → **登记为已知例外,不再作为待决策项挂账**。
|
||||
|
||||
⚠ 差异路径原属本类,**已由补1 转入类A**(必须落 kpi_value_id,不可归期即跳过)——
|
||||
其守卫与负对照用例见 `tests/test_alert_caliber_r5_residual_20260919.py`,
|
||||
本类不再保留其「无期间关联」的现状断言(该断言与补1 冲突,已随裁定删除)。
|
||||
类B 余族的现状登记同样落在该新套件(`TestR5ResidualClassBStatusQuo`)。
|
||||
"""
|
||||
|
||||
def test_deviation_path_row_has_no_derivable_period(self, db: Session, monkeypatch):
|
||||
"""差异预警路径(cron daily_sync 第 4 步):期间为规整月,但行内无 kpi_value 关联
|
||||
⇒ 库内派生期间 = NULL(id 17 同类形态,可复现)"""
|
||||
import app.utils.deviation_engine as de
|
||||
|
||||
_ent(db, 1, "active")
|
||||
create_test_kpi(db, kpi_code="C_NEW_CLIENTS", kpi_name="新客户数", entity_id=1, unit="家")
|
||||
monkeypatch.setattr(de, "build_deviation_alert",
|
||||
lambda db_s, kpi, period, *a, **kw: {
|
||||
"triggered": True, "kpi_alert_level": "red", "alert_type": "budget",
|
||||
"suggestion": None,
|
||||
"deviation": {"actual_value": 88.8, "budget_value": 10.0,
|
||||
"deviation_rate": 788.0}})
|
||||
monkeypatch.setattr(de, "check_trend_anomaly", lambda *a, **kw: {"anomaly": False})
|
||||
|
||||
n = de.run_deviation_check(db, period="2026-07")
|
||||
db.expire_all()
|
||||
|
||||
rows = db.query(KPIAlert).all()
|
||||
assert n == 1 and len(rows) == 1
|
||||
assert rows[0].kpi_value_id is None, "差异预警不落 kpi_value 关联"
|
||||
assert "[差异预警]" in rows[0].alert_message and "2026-07" in rows[0].alert_message, \
|
||||
"期间存在于文案中,但不可由库派生(类B:待产品侧决定是否补关联)"
|
||||
# 原 `test_deviation_path_row_has_no_derivable_period`(差异路径「无期间关联」现状断言)
|
||||
# 已随 §补充裁定 R5-补1 删除:差异路径转为类A,必须落 kpi_value_id,不可归期即跳过。
|
||||
# 替代用例(含负对照:旧逻辑取 NULL / 新逻辑取该期间行)见
|
||||
# tests/test_alert_caliber_r5_residual_20260919.py::TestR5ResidualDeviationLinkage(6 例)。
|
||||
|
||||
Reference in New Issue
Block a user