fix(alerts): enforce caliber ruling R5

- alert_entity_scope: add is_attributable_period / guard_alert_period (R5) and entity_alert_guard (R1, any write path)
- alert_rules.run_all_alert_checks: skip non-YYYY-MM period alert rows (R5)
- bot_bridge / bot_bridge_v2: R1 demo-entity guard + R5 period guard on write paths
- tests: new R5 suite (10 cases)

Not pushed / not deployed (release gate held by 任总).
This commit is contained in:
Hermes CI Fix
2026-09-19 10:42:09 +08:00
parent 3beec14d96
commit 5a99ed87e0
5 changed files with 347 additions and 6 deletions
+21 -3
View File
@@ -20,6 +20,8 @@ from app.models.budget_plan import BudgetPlan
from app.models.cost_model import StandardCost, ActualCost, AbcActivity, AbcAllocation
from app.models import KPICausality
from app.risk_levels import risk_level
# 裁定 R1/R5(2026-09-19 财务口径):demo 账套任何写通道不产生业务预警;期间非规整月不入库(单一事实源)
from app.utils.alert_entity_scope import entity_alert_guard, guard_alert_period
import json
logger = logging.getLogger("cma.bot_bridge")
@@ -673,6 +675,10 @@ def bot_kpi_value_with_check(data: dict, db: Session = Depends(get_db), bot: dic
raise HTTPException(404, "KPI不存在")
period = period or datetime.now().strftime("%Y-%m")
# R1(裁定 2026-09-19):「R1 与入口无关」——Bot 通道按显式 entity 写入同样不得产出业务预警
alerts_allowed = entity_alert_guard(db, entity_id, source="bot_bridge.kpi-value-with-check",
detail="kpi_id=%s" % kpi_id)
# ① 写值(upsert)
existing = db.query(KPIValue).filter(
KPIValue.kpi_id == kpi_id, KPIValue.period == period).first()
@@ -689,7 +695,9 @@ def bot_kpi_value_with_check(data: dict, db: Session = Depends(get_db), bot: dic
# ② 跑该KPI关联的预警规则(复用 alert_rules 检查函数,非全量)
alerts = []
if run_check:
# R5(裁定 2026-09-19):期间非规整月(含伪期间)→ 不入库,跳过并记数
skipped_non_month_period = 0
if run_check and alerts_allowed:
from app.api.alert_rules import AlertRule, _check_static, _check_dynamic, _check_trend
rules = db.query(AlertRule).filter(
AlertRule.kpi_id == kpi_id, AlertRule.entity_id == entity_id,
@@ -709,6 +717,11 @@ def bot_kpi_value_with_check(data: dict, db: Session = Depends(get_db), bot: dic
else:
continue
if level and level != "green":
# R5 生成侧守卫(裁定 2026-09-19):期间非规整月 → 该 kpi_alerts 行不入库
if not guard_alert_period(period, source="bot_bridge.kpi-value-with-check",
detail="kpi_id=%s rule_id=%s" % (kpi_id, rule.id)):
skipped_non_month_period += 1
continue
dup = db.query(KPIAlert).filter(
KPIAlert.kpi_id == kpi_id, KPIAlert.kpi_value_id == kv.id,
KPIAlert.alert_level == level, KPIAlert.status == "pending").first()
@@ -720,8 +733,13 @@ def bot_kpi_value_with_check(data: dict, db: Session = Depends(get_db), bot: dic
except Exception as e:
logger.warning(f"聚合检查失败 rule={rule.id}: {e}")
db.commit()
return {"kpi_id": kpi_id, "kpi_code": kpi.kpi_code, "value": float(actual_value),
"period": period, "alerts": alerts, "status": "ok"}
resp = {"kpi_id": kpi_id, "kpi_code": kpi.kpi_code, "value": float(actual_value),
"period": period, "alerts": alerts, "status": "ok",
"skipped_non_month_period": skipped_non_month_period}
if not alerts_allowed:
# R1(裁定 2026-09-19):demo 账套在 Bot 通道同样不产生业务预警(值照写、预警跳过)
resp.update({"skipped": True, "reason": "R1_demo_entity_excluded"})
return resp
@router.post("/kpis/create-with-links")