feat(sense): T-X5 云端评判晋升表×conformal 双闸门——本地档自动接管(采纳 cortiq promotion)

- gateway/sense/promotion.py:PromotionTable 状态机(candidate/promoted/demoted)
  * 晋升门:n_total>=n_min 且 通过率>=promote_lb 且 soak 浸泡期满足
  * 退化:promoted 期间通过率<demote_lb 自动降级;降级后可凭数据恢复
  * 一票否决:tier=T3 观察不进通过率统计,已晋升者立即降级
  * 持久化 sense_promotion 表(CREATE IF NOT EXISTS 幂等)
- grader:live 模式对已晋升标签的 T2 决策(无 T3 信号/非 fallback)升级 T1,
  hard_gates.promotion_applied 如实标注;T1/T3/fallback 路径不受影响
- 回填链路:T1 审计抽样带 promo:<label> 标签 -> 人工 verdict approve=ok
  经 /review/{id} 提交时回填晋升表(失败不影响审核主流程)
- 双闸门语义:conformal 阈值保证单条决策风险率,晋升表保证标签级接管节奏;
  collect 数据不足(n_min 未满)不晋升,全部迁移可经 T-X4 留痕审计

pytest 474 passed(T-X4 后 466 + 8)
This commit is contained in:
tzt
2026-09-18 23:01:52 +08:00
parent 7e01dc9b16
commit 1aaa05cacc
7 changed files with 334 additions and 5 deletions
+17 -2
View File
@@ -21,6 +21,7 @@ from gateway.sense.decision_cache import DecisionCache
from gateway.sense.embedder import embed
from gateway.sense.errors import EmbedderDown
from gateway.sense.features import gate
from gateway.sense.promotion import promotion_label
from gateway.sense.store import json_dumps
@@ -99,10 +100,11 @@ def _rejected_tiers(fallback: bool, probs: Dict[str, float],
class Grader:
"""决策器(持有 active 工件缓存;工件/阈值切换后调 invalidate)。"""
def __init__(self, cfg: SenseConfig, store, observer=None):
def __init__(self, cfg: SenseConfig, store, observer=None, promotion=None):
self.cfg = cfg
self.store = store
self.observer = observer
self._promotion = promotion # T-X5:晋升表(可 None
self._head: Optional[LinearHead] = None
self._head_loaded = False
self._thresholds: Optional[Dict[str, Any]] = None
@@ -194,6 +196,18 @@ class Grader:
else:
tier = "T2"
# ---- 晋升表应用(T-X5conformal 之外的统计闸门;仅 live----
# 仅对 T2 决策且无任何 T3 信号时,允许已晋升标签升级 T1;
# T1/T3 决策与 fallback 路径不受晋升表影响。
promotion_applied = False
if (self._promotion is not None and self.cfg.mode == "live"
and not fallback and tier == "T2"
and not feats.repo_signals
and probs.get("t3", 0.0) < float(th.get("t3", 0.9))):
if self._promotion.is_promoted(promotion_label(consumer, domain)):
tier = "T1"
promotion_applied = True
# ---- mode 裁剪(D-G7----
if self.cfg.mode == "live":
executed = tier # live:决策即执行
@@ -208,7 +222,8 @@ class Grader:
"intent_blocked": feats.intent_blocked,
"repo_signals": feats.repo_signals,
"over_length": feats.over_length,
"t1_hard_ok": feats.t1_hard_ok},
"t1_hard_ok": feats.t1_hard_ok,
"promotion_applied": promotion_applied},
thresholds_version=th_version, head_version=head_version,
mode=self.cfg.mode, fallback=fallback,
executed_tier=executed)
+81
View File
@@ -0,0 +1,81 @@
"""云端评判式晋升表(T-X5,采纳 cortiq promotion.rs,与 conformal 双闸门)。
定位:conformal 阈值给出「分对率 >= 1-α」的统计保证(单条查询粒度),
本表在其之上给出「某类任务(label)可由本地档接管」的**节奏自动化**:
- 观察:人工核验 verdictapprove=ok)等质量信号按 label 累计通过率;
- 晋升:n_total >= n_min 且 通过率 >= promote_lb 且 soak 浸泡期满足
-> state=promoted(此后 grader 对该 label 的 T2 决策可升级 T1);
- 退化:promoted 期间通过率 < demote_lb -> demoted(自动回退分级路径);
- 一票否决:tier=T3(高复杂度/仓库级)观察不进通过率统计,且若已晋升
立即降级——与 cortiq "HIGH tier escalation is served by the cloud" 同构。
灰度纪律(沿用 D-G7):collect 数据不足(n_min 未满)不得晋升;
全部状态迁移可由 T-X4 的决策留痕审计。
"""
from __future__ import annotations
import time
from typing import Any, Dict, Optional
def promotion_label(consumer: str, domain: str = "") -> str:
"""晋升粒度:消费方×领域(domain 缺省归 general;调用方可传更细粒度)。"""
return f"{consumer or 'default'}:{domain or 'general'}"
class PromotionTable:
"""标签晋升状态机(candidate / promoted / demoted;持久化于 sense_promotion)。"""
def __init__(self, store, now=None, n_min: int = 20, promote_lb: float = 0.95,
soak_days: float = 3.0, demote_lb: float = 0.85,
high_tier_veto: bool = True):
self.store = store
self._now = now or (lambda: time.time())
self.n_min = max(1, int(n_min))
self.promote_lb = float(promote_lb)
self.demote_lb = float(demote_lb)
self.soak_s = float(soak_days) * 86400.0
self.high_tier_veto = bool(high_tier_veto)
def observe(self, label: str, ok: bool, tier: str = "",
ts: Optional[float] = None) -> Dict[str, Any]:
"""记录一次质量信号并推进状态机;返回迁移后的行。"""
ts = float(ts) if ts is not None else float(self._now())
row = self.store.get_promotion(label) or {
"label": label, "n_total": 0, "n_ok": 0, "state": "candidate",
"first_ts": int(ts), "promoted_ts": 0, "last_ts": 0}
# 一票否决:T3 观察不进通过率统计;已晋升者立即降级
if self.high_tier_veto and str(tier).upper() == "T3":
row["last_ts"] = int(ts)
if row["state"] == "promoted":
row["state"] = "demoted"
row["promoted_ts"] = 0
self.store.upsert_promotion(row)
return row
row["n_total"] = int(row["n_total"]) + 1
row["n_ok"] = int(row["n_ok"]) + (1 if ok else 0)
row["last_ts"] = int(ts)
rate = row["n_ok"] / row["n_total"] if row["n_total"] else 0.0
soaked = (ts - int(row["first_ts"])) >= self.soak_s
if row["state"] == "promoted":
if row["n_total"] >= self.n_min and rate < self.demote_lb:
row["state"] = "demoted" # 退化自动降级
row["promoted_ts"] = 0
else:
# candidate / demoted 共用同一晋升门(降级后可凭数据恢复)
if row["n_total"] >= self.n_min and rate >= self.promote_lb and soaked:
row["state"] = "promoted"
row["promoted_ts"] = int(ts)
self.store.upsert_promotion(row)
return row
def is_promoted(self, label: str) -> bool:
row = self.store.get_promotion(label)
return bool(row) and row["state"] == "promoted"
def snapshot(self, label: str) -> Optional[Dict[str, Any]]:
return self.store.get_promotion(label)
+32
View File
@@ -41,6 +41,14 @@ CREATE TABLE IF NOT EXISTS sense_decisions(
candidate_scores TEXT NOT NULL DEFAULT '{}',
rejected TEXT NOT NULL DEFAULT '[]');
CREATE INDEX IF NOT EXISTS idx_dec_ts ON sense_decisions(ts);
CREATE TABLE IF NOT EXISTS sense_promotion(
label TEXT PRIMARY KEY, n_total INTEGER NOT NULL DEFAULT 0,
n_ok INTEGER NOT NULL DEFAULT 0,
state TEXT NOT NULL DEFAULT 'candidate',
first_ts INTEGER NOT NULL DEFAULT 0,
promoted_ts INTEGER NOT NULL DEFAULT 0,
last_ts INTEGER NOT NULL DEFAULT 0);
"""
@@ -178,6 +186,30 @@ class SenseStore:
out.append(d)
return out
# ---------- 晋升表(T-X5:本地档自动接管的双闸门之一) ----------
def upsert_promotion(self, row: Dict[str, Any]) -> None:
with self._lock, self._connect() as conn:
conn.execute(
"""INSERT OR REPLACE INTO sense_promotion
(label, n_total, n_ok, state, first_ts, promoted_ts, last_ts)
VALUES (?,?,?,?,?,?,?)""",
(row["label"], int(row.get("n_total", 0)),
int(row.get("n_ok", 0)), str(row.get("state", "candidate")),
int(row.get("first_ts", 0)), int(row.get("promoted_ts", 0)),
int(row.get("last_ts", 0))))
def get_promotion(self, label: str) -> Optional[Dict[str, Any]]:
with self._lock, self._connect() as conn:
row = conn.execute(
"SELECT * FROM sense_promotion WHERE label = ?", (label,)).fetchone()
return dict(row) if row else None
def list_promotions(self) -> List[Dict[str, Any]]:
with self._lock, self._connect() as conn:
rows = conn.execute(
"SELECT * FROM sense_promotion ORDER BY last_ts DESC").fetchall()
return [dict(r) for r in rows]
# ---------- 工件 ----------
def register_artifact(self, version: str, kind: str, path: str,
metrics: Dict[str, Any], active: bool = False,