12.3 误报与白名单治理

门禁一定会误报,白名单就是给经过判断的例外开的口子;但它一旦没有纪律就会烂成垃圾场。本节把白名单当成要检查的对象:4 条登记先过五道治理关(重复/级别不可压制/缺理由/缺负责人/过期),只有 2 条生效、压掉 2 条 warn,而 block 级硬失败永远压不住,门禁压完仍是 block。

12.3 误报与白名单治理

门禁一定会误报——规则是死的,资产是活的:竖版 2:3 的横幅天生不是 2 的幂,一次性远景背景就是可以超尺寸。白名单就是给这些经过判断的例外开的口子。但白名单一旦没有纪律,就会从「例外清单」烂成「垃圾场」,最后没人记得哪条还能删。本节把白名单也当成要检查的对象:每条登记先过五道治理关,过不了的不生效。

级别沿用 12.2:只有 warn / info 可压制,block 级(硬失败)永远压不住——这不是实现细节,是原则。

12.3.1 五道治理关

顺序问题码判据
1E_DUPLICATE(code, id) 已被前一条登记占用
2E_NOT_SUPPRESSIBLElevel 不在 suppressibleLevels(即 block)
3E_NO_REASON缺理由(requireReason)
4E_NO_OWNER缺负责人(requireOwner)
5E_EXPIREDexpiresOn < today(requireExpiry)

顺序是固定的——重复检查命中就立刻返回 ["E_DUPLICATE"],后面的检查不再跑。固定顺序是为了跨语言逐位一致:Python 与 TS 必须按同一顺序、同一短路规则产出同一串问题码,否则 digest 对不上。

12.3.2 白名单配置:4 条登记,2 条干净

配置里有 4 条登记,只有 2 条是「干净」的,另外 2 条故意埋了问题:一条是给 block 级开的(会被 E_NOT_SUPPRESSIBLE 拒),一条缺理由缺负责人(会被 E_NO_REASON + E_NO_OWNER 拒),同时它与第 1 条 (code, id) 重复(会被 E_DUPLICATE 短路拒)。today 是 2026-10-13,所以那条 expiresOn: 2026-09-30 的登记已过期:

{
  "whitelistSet": "ci-gate",
  "specVersion": 1,
  "today": "2026-10-13",
  "suppressionBudget": 3,
  "policy": {
    "requireReason": true,
    "requireOwner": true,
    "requireExpiry": true,
    "suppressibleLevels": ["warn", "info"],
    "note": "白名单只压制 warn / info;block 级(硬失败)不可压制——治理规则拒绝为硬失败开白名单。每条登记必须有理由、有负责人、有到期日;到期即失效并重新上报。"
  },
  "entries": [
    { "code": "E_SIZE_POT", "id": "tex_banner_hd", "level": "warn", "reason": "竖版 2:3 横幅,非 2 的幂是刻意的", "owner": "ta-lead", "expiresOn": "2026-12-31", "ticket": "ART-412" },
    { "code": "E_SIZE_MAX", "id": "tex_huge_bg", "level": "warn", "reason": "一次性远景背景,压缩后已确认", "owner": "ta-lead", "expiresOn": "2026-11-30", "ticket": "ART-418" },
    { "code": "E_NAME", "id": "tex_legacy_upper", "level": "block", "reason": "历史遗留,下版重命名", "owner": "art-pm", "expiresOn": "2026-09-30", "ticket": "ART-330" },
    { "code": "E_SIZE_POT", "id": "tex_banner_hd", "level": "warn", "reason": "", "owner": "", "expiresOn": "2026-12-31", "ticket": "ART-455" }
  ]
}

12.3.3 S 层:先体检,再压制

读 12.1 的 finding 清单与白名单,先跑治理体检,再用通过体检的登记做压制。治理问题与压制结果都只含字符串,排序后取 digest:

#!/usr/bin/env python3
# file: sim/ta12_suppress.py
# tier: S
# verify: /opt/homebrew/bin/python3 sim/ta12_suppress.py
"""卷二 12.3 规则误报与白名单治理(S 层)。读 12.1 的 out/ta12_engine.json 与
config/ta12_whitelist.json,做两件事:

1. **治理体检**——每条登记按固定顺序过五道关:重复(E_DUPLICATE)→ 级别不可压制
   (E_NOT_SUPPRESSIBLE)→ 缺理由(E_NO_REASON)→ 缺负责人(E_NO_OWNER)→ 已过期
   (E_EXPIRED)。有任何一条治理问题的登记**不生效**。
2. **压制**——只有通过体检的登记才按 (code, id) 精确压制对应 finding;block 级永远压不住。

治理体检的输出与压制结果都只含字符串,落 out/ta12_suppress.json 供 P 层逐位对照。
"""
import hashlib
import json
import os
import sys

ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))


def audit(entry, seen, policy, today):
    """返回该登记的治理问题码列表(空 = 通过)。检查顺序固定,保证跨语言一致。"""
    out = []
    key = (entry["code"], entry["id"])
    if key in seen:
        return ["E_DUPLICATE"]
    if entry["level"] not in policy["suppressibleLevels"]:
        out.append("E_NOT_SUPPRESSIBLE")
    if policy["requireReason"] and not entry["reason"]:
        out.append("E_NO_REASON")
    if policy["requireOwner"] and not entry["owner"]:
        out.append("E_NO_OWNER")
    if policy["requireExpiry"] and entry["expiresOn"] < today:
        out.append("E_EXPIRED")
    return out


def main():
    engine = json.load(open(os.path.join(ROOT, "out/ta12_engine.json"), encoding="utf-8"))
    wl = json.load(open(os.path.join(ROOT, "config/ta12_whitelist.json"), encoding="utf-8"))
    policy, today = wl["policy"], wl["today"]
    findings = engine["findings"]

    seen, active, governance = set(), set(), []
    for e in wl["entries"]:
        problems = audit(e, seen, policy, today)
        if problems:
            for p in problems:
                governance.append({"code": p, "id": e["id"], "detail": f"{e['code']}/{e['id']}"})
        else:
            active.add((e["code"], e["id"]))
        seen.add((e["code"], e["id"]))

    suppressed, remaining = [], []
    for f in findings:
        (suppressed if (f["code"], f["id"]) in active else remaining).append(f)
    suppressed.sort(key=lambda f: (f["code"], f["id"]))
    governance.sort(key=lambda g: (g["code"], g["id"]))

    gov_lines = [f"{g['code']}:{g['id']}" for g in governance]
    sup_lines = [f"SUPPRESSED:{s['code']}:{s['id']}" for s in suppressed]
    digest = hashlib.sha256("\n".join(sorted(gov_lines) + sorted(sup_lines)).encode("utf-8")).hexdigest()[:16]
    remaining_block = sum(1 for f in remaining if f["severity"] == "block")
    rep = {
        "project": engine["project"],
        "whitelistSet": wl["whitelistSet"],
        "today": today,
        "findingCount": len(findings),
        "entryCount": len(wl["entries"]),
        "activeSuppressions": len(active),
        "suppressionBudget": wl["suppressionBudget"],
        "budgetOk": len(active) <= wl["suppressionBudget"],
        "suppressedCount": len(suppressed),
        "suppressed": [{"code": f["code"], "id": f["id"]} for f in suppressed],
        "remainingCount": len(remaining),
        "remainingBlock": remaining_block,
        "gateAfter": "block" if remaining_block else "pass",
        "governanceFindings": governance,
        "digest": digest,
    }
    with open(os.path.join(ROOT, "out/ta12_suppress.json"), "w", encoding="utf-8") as f:
        json.dump(rep, f, ensure_ascii=False, indent=2)
        f.write("\n")

    print(f"entries={rep['entryCount']} active={rep['activeSuppressions']} "
          f"suppressed={rep['suppressedCount']} remaining={rep['remainingCount']} "
          f"gateAfter={rep['gateAfter']}")
    print(f"budgetOk={rep['budgetOk']} governance={len(governance)} digest={digest}")
    for g in governance:
        print(f"  {g['code']:20s} {g['detail']}")
    for s in suppressed:
        print(f"  SUPPRESSED {s['code']:16s} {s['id']}")
    print("wrote out/ta12_suppress.json")
    return 0


if __name__ == "__main__":
    sys.exit(main())

实跑 /opt/homebrew/bin/python3 sim/ta12_suppress.py:

entries=4 active=2 suppressed=2 remaining=5 gateAfter=block
budgetOk=True governance=3 digest=52188a151fd1b734
  E_DUPLICATE          E_SIZE_POT/tex_banner_hd
  E_EXPIRED            E_NAME/tex_legacy_upper
  E_NOT_SUPPRESSIBLE   E_NAME/tex_legacy_upper
  SUPPRESSED E_SIZE_MAX       tex_huge_bg
  SUPPRESSED E_SIZE_POT       tex_banner_hd
wrote out/ta12_suppress.json

4 条登记里 2 条生效、压掉 2 条 warn;治理报出 3 条问题。E_NAME/tex_legacy_upper 同时命中两条——它是 block 级(E_NOT_SUPPRESSIBLE)且已过期(E_EXPIRED);E_SIZE_POT/tex_banner_hd 的第 2 条登记命中 E_DUPLICATE(与第 1 条重复,直接短路,所以没再报 E_NO_REASON / E_NO_OWNER)。压掉 2 条 warn 之后 remaining=5 全是 block,门禁仍然 block——这正是设计意图。

12.3.4 P 层:TS 内核与逐位对拍

TS 内核与 Python 逐条对应,治理问题码与压制结果都只含字符串,因此 digest 必须逐位一致:

// file: core/src/ta12/suppress.ts
// tier: P
// verify: node --test 'core/test/ta12_*.test.ts'
// 卷二 12.3 白名单治理的 TS 侧实现。与 sim/ta12_suppress.py 同一套口径:登记先过五道治理关
// (重复 → 级别不可压制 → 缺理由 → 缺负责人 → 已过期),有任一问题的登记不生效;只有通过的
// 登记才按 (code, id) 精确压制,block 级永远压不住。只允许可擦除 TS 语法。
export type Finding = { code: string; id: string; detail: string; severity: string; hardFail: boolean };
export type Entry = { code: string; id: string; level: string; reason: string; owner: string; expiresOn: string; ticket: string };
export type Policy = { requireReason: boolean; requireOwner: boolean; requireExpiry: boolean; suppressibleLevels: string[] };
export type GovFinding = { code: string; id: string; detail: string };

const key = (code: string, id: string): string => `${code}::${id}`;
const cmp = (a: string, b: string): number => (a < b ? -1 : a > b ? 1 : 0);

/** 该登记的治理问题码列表(空 = 通过)。检查顺序固定,保证跨语言一致。 */
export function audit(entry: Entry, seen: Set<string>, policy: Policy, today: string): string[] {
  const out: string[] = [];
  if (seen.has(key(entry.code, entry.id))) return ["E_DUPLICATE"];
  if (!policy.suppressibleLevels.includes(entry.level)) out.push("E_NOT_SUPPRESSIBLE");
  if (policy.requireReason && !entry.reason) out.push("E_NO_REASON");
  if (policy.requireOwner && !entry.owner) out.push("E_NO_OWNER");
  if (policy.requireExpiry && entry.expiresOn < today) out.push("E_EXPIRED");
  return out;
}

export function applySuppression(findings: Finding[], entries: Entry[], policy: Policy, today: string) {
  const seen = new Set<string>();
  const active = new Set<string>();
  const governance: GovFinding[] = [];
  for (const e of entries) {
    const problems = audit(e, seen, policy, today);
    for (const p of problems) governance.push({ code: p, id: e.id, detail: `${e.code}/${e.id}` });
    if (problems.length === 0) active.add(key(e.code, e.id));
    seen.add(key(e.code, e.id));
  }
  const suppressed = findings.filter((f) => active.has(key(f.code, f.id)));
  const remaining = findings.filter((f) => !active.has(key(f.code, f.id)));
  suppressed.sort((a, b) => cmp(a.code, b.code) || cmp(a.id, b.id));
  governance.sort((a, b) => cmp(a.code, b.code) || cmp(a.id, b.id));
  return { suppressed, remaining, governance, activeSuppressions: active.size };
}

实跑 node --test 'core/test/ta12_*.test.ts'(本节 4 条):

suppress digest = 52188a151fd1b734
✔ 跨语言:治理体检与压制结果 == Python 产物(逐位一致)
✔ 样本:4 条登记 → 2 条生效、3 条治理问题,压掉 2 条 warn,门禁仍 block
✔ 负例:给硬失败开白名单不生效,block 级 finding 必须仍在
✔ 负例:过期登记不生效;空白名单不压任何东西
ℹ pass 12
ℹ fail 0

两侧 digest 都是 52188a151fd1b734。两条负例各堵一个漏洞:一条给 E_NAME(block 级)开白名单,断言 suppressed.length === 0 且 E_NAME 仍在 remaining 里;一条用过期的 E_SIZE_POT 登记与空白名单分别跑,断言压不掉任何东西。

12.3.5 为什么 block 永远压不住,以及配额与到期日

白名单能压 warn、能压 info,就是压不住 block。理由回到 12.1:那五类硬失败的后果是不可逆的(引用会断、包发不出去),把它「记一笔放行」等于把不可逆的破坏写进流水线。所以治理规则主动拒绝为 block 开白名单——不是压了没效果,而是这条登记本身就被判为治理问题(E_NOT_SUPPRESSIBLE),根本进不了生效集合。

第二条纪律是配额:suppressionBudget=3,生效登记数不得超过它,本节 2 ≤ 3 通过。配额的意义不是限制数量,而是逼你清理——满了就必须先删一条旧的才能加新的,白名单因此不会无限膨胀。第三条是到期日:登记不是永久的,expiresOn 一过就失效并重新上报,强制每条例外定期被重新审视。三条纪律合起来,白名单才是「有记账的例外」,而不是「静音的开关」。

12.3.6 本章收口:从判据到门禁

第 12 章三节其实是一条线:12.1 把各章判据收进一台引擎,12.2 决定拿什么当阻断条件,12.3 管住例外怎么开、怎么清。三节共用一份资产样本、一套级别语义、一个 digest 对拍机制——规则引擎不是新造一套,而是第 3/4/5 章判据的统一消费方。下一章进入中台:当这些检查从「一个仓库的 CI」变成「很多团队的公共服务」,要考虑的就完全是另一组问题了。

阅读导航:上一节:12.2 门禁的分级与阻断策略 · 下一节:13.1 中台要解决什么问题 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「gameart」更多文章

  1. 附录 A:风格参数表、量化规则与数据集索引
  2. 9.3 等距资产的制作约束
  3. 9.2 等距 tile 与遮挡规则