# -*- coding: utf-8 -*- """judge_audit —— **判据元体检**(2026-10-04 建) 一句话:**给判据本身做体检**,防止「判据恒绿 / 假绿」。 🔴🔴 为什么必须有这个工具(⛔ 不是又写个检查) 今天栽的坑收敛后**只有两个根因**,都不是「不小心」,都是**结构性的**: **A. 判据与被检对象之间的「连接」没被验证** 判据**看起来**在验某功能,实际压根没连上被检对象 ⇒ 恒绿。今天至少 6 次: · V4 判据里**自己重算**一遍算法(`lambda`),与被检的 `_norm_ws` 无关 · V6 锚在「源码里出现过 `automations` 这个**词**」⇒ 把查询改空照样绿 · 夹具只覆盖**顺带**跑到的分支(写回那步恒空转 ⇒ 判据删了它照样全绿) · 锚点**凭记忆**写(文档写「正确处置=」我写「正解=」⇒ 基线直接红) ⚠️ 共同点:**判据绿灯与被检对象状态无关**。 **B. 验证动作本身出错,而我不检查它** · 变异脚本自己 `IndexError`(`sys.argv` 少传参)崩掉,**照样打出 PASS 2/FAIL 0** · 变异后**只重置一份**文件 ⇒ 下一轮累积污染 ⇒ 后面几轮结论全作废 ⚠️ 共同点:**验证器自己的失败被当成了结论**。 ⇒ 纪律(`references/00-动手前必过.md`)挡不住这两条 —— 今天写了一整天纪律, 该栽还是栽。**根因是「靠记得」⇒ 解法只能是「机器强制」**。 ⇒ 本工具做的就是把这两条**变成读数**:⛔ 你不跑它,你自己看不见。 用法(三个都只读,⛔ 不写任何文件) 1) 元体检(扫 selftest.py 里所有判据的写法): python judge_audit.py --audit selftest.py 2) 变异体检(给某条判据造变异,验证它真会红): python judge_audit.py --mutate <被检文件> "<锚点>" "<替换成>" -k <用例关键字> 3) 列已知弱锚点清单(自测要断言这个表非空): python judge_audit.py --list-rules """ from __future__ import annotations import argparse import hashlib import io import os import re import shutil import subprocess import sys import tempfile from pathlib import Path try: sys.stdout.reconfigure(encoding="utf-8", errors="replace") except Exception: pass HERE = Path(__file__).resolve().parent SELFTEST = HERE / "selftest.py" PY = sys.executable # ── 元规则表:⛔ 每条都注明「今天栽在哪」 ──────────────────────────────── # kind: weak = 弱锚点(只证明「文本出现过」,不证明「行为对」) # self = 自我重算(判据自己算一遍,与被检对象无关) # weak_ev= 弱行为判据(跑了函数但只查一个弱条件) RULES = [ # ---- A 类:判据没连上被检对象 ---- ("self-recompute", "self", # 🔴 2026-10-04 修:原式 `=\s*lambda\s+\w+\s*:` **漏抓 `(lambda s: …)`**(带括号的写法) # ⇒ 实测探针明明写了 `(lambda s: s.lower())` 却只命中 1 类规则, # 判据却断言「≥2 类」⇒ **假红**。⇒ 括号可选,并允许更宽的形参。 r"=\s*\(?\s*lambda\s[\s\w,*]*:", "判据里用 `lambda` **自己重算**被检的算法 ⇒ 改被检对象它照样绿(今天 V4)"), ("anchor-text-only", "weak", r"\b(in|not in)\s+(src|T|SK|TXT|txt|body|content)\b", "锚在「源码/文本里出现过某词」⇒ ⛔ 证明不了行为(今天 V6;判据元规则之一)"), ("anchor-imagined", "weak", r"正解=|正确处置=|正确处置= ", "锚点是**凭记忆重写**的措辞 ⇒ 文档微调就假红/假绿(今天多次)"), # ---- B 类:验证器自身的失败被当成结论 ---- ("mutate-no-md5", "self", r"def\s+mut\w*\(.*\):(?![^}]*md5)", "变异函数**不核对 md5** ⇒ 变异没真植入时照样打 PASS(今天 `sys.argv` 少传参那次)"), ("no-restore-check", "self", r"replace\(a,\s*b\s*,\s*1\)(?![^)]*\n[^)]*(md5|restore|还原))", "只 `replace` 不**核对还原** ⇒ 多轮变异互相污染(今天累积污染那次)"), # ---- 附带的经典坑 ---- ("glob-existence", "weak_ev", r"glob\([^)]*\*\*[^)]*\)[^\n]*(exists\(\)|不存在|零命中)", "用 `glob('**/x')` 判「文件不存在」⇒ ⛔ `**` **不穿透点目录**(今天栽过 4 次)"), ("count-without-filter", "weak_ev", r"count\(\s*[\"']✗[\"']\s*\)|\.count\(b?[\"']✗", "数 `✗` **不排除「报告型」行** ⇒ 报告型用例一多就误判(今天栽过)"), ] # ── 误报白名单:⛔ 元规则表本身**也会**假绿/假红(元规则也判不准) ────── # 🔴 2026-10-04 实测踩到:`glob-existence` 规则把**教训本身**当成了错误写法 —— # 那处 `import glob` 正是「自建夹具证明 glob 会漏」这条判据的**被测行为**, # 不是"用 glob 判文件不存在"的错误用法。⇒ 扫写法天然分不清 # 「引用教训」与「犯错误」。⇒ 只能显式列出已知误报。 # ⛔ 代价:白名单要人维护 ⇒ 每加一条误报必须回来加一行(宁可漏报,不许把真问题压掉)。 FALSE_POSITIVE = { # 行号 → 原因。行号随文件变动会漂 ⇒ 漂了就当没命中(宁可漏报)。 "selftest.py:2762": "`import glob` 是**被测行为**(自建夹具证明 glob 会漏)" "⛔ 不是「用 glob 判文件不存在」的错误写法", } def _fp_key(path: Path, line: int) -> str: return "%s:%d" % (Path(path).name, line) def _md5(p) -> str: return hashlib.md5(Path(p).read_bytes()).hexdigest() def _lines(path): out = [] for i, ln in enumerate(Path(path).read_text(encoding="utf-8", errors="replace").splitlines(), 1): out.append((i, ln)) return out # ── ① 元体检 ──────────────────────────────────────────────────────────── def audit(target: Path) -> int: src = target.read_text(encoding="utf-8", errors="replace") hits = [] fps = [] for name, kind, pat, why in RULES: rx = re.compile(pat, re.M) for i, ln in _lines(target): if rx.search(ln): k = _fp_key(target, i) if k in FALSE_POSITIVE: fps.append({"line": i, "why": FALSE_POSITIVE[k], "rule": name}) continue hits.append({"rule": name, "kind": kind, "line": i, "why": why, "text": ln.strip()[:96]}) by = {} for h in hits: by.setdefault(h["rule"], []).append(h["line"]) print("=" * 72) print("判据元体检 · %s" % target) print("=" * 72) if fps: print(" ⓘ 已知误报 %d 处(⛔ 已人工判定为「引用教训」⛔ 不是错误写法):" % len(fps)) for f in fps[:5]: print(" · 行 %d [%s] %s" % (f["line"], f["rule"], f["why"][:70])) if len(fps) > 5: print(" … 另有 %d 处" % (len(fps) - 5)) print() if not hits: print(" ✅ 未命中任何已知弱锚点模式") print("\n ⓘ ⛔ **「没命中」不等于「没有」** —— 本表只覆盖已知的坑,") print(" 新形态的弱锚点要**手写变异**才抓得到 ⇒ 仍须 `--mutate`。") return 0 print(" ⚠️ 命中 %d 处(⛔ 这些是**可疑写法**,⛔ 不等于一定是错):\n" % len(hits)) for name, kind, _pat, why in RULES: if name not in by: continue print(" 🔴 %-20s [%s] 行 %s" % (name, kind, by[name][:8])) print(" ↳ %s\n" % why) print(" 处置:逐条改判据(⛔ 别改规则表消警告 —— 那是把体温计砸了)") print(" ⓘ 确认是误报就加进 `FALSE_POSITIVE`(⛔ 但要写清为什么,⛔ 不许默默加)") return len(hits) def do_mutate(tgt: Path, old: str, new: str, kw: str, cases: int = 1) -> int: """真植入变异 ⇒ 跑判据 ⇒ 必须报红 ⇒ 还原并**核对 md5**。 🔴🔴 2026-10-04 修(今天第三次栽在"验证器自己出错却输出结论"): 原实现把被检文件**拷到临时目录**再跑 selftest ⇒ 但 `selftest.py` 里 `HERE = Path(__file__).resolve().parent` 加载的是**原目录**下那份 ⇒ **变异根本没进被检对象** ⇒ 判据当然不报红,而输出却写着 「⛔ 没报红 ❌」+ rc=0(看着像"验证过了,实际什么也没验证")。 ⇒ 改成**原地改 → 跑 → `finally` 还原**(还原有 md5 兜底)。 🔴 本函数自己就是「B 类根因」的教训对象,故: · 变异**没真植入**(`old` 不在)⇒ rc=2,⛔ **绝不**输出"通过" · 还原后**核对 md5** ⇒ 对不上 rc=2(⛔ 环境被污染,后续结论全作废) · 判据输出**同时**报 rc 与是否报红 ⇒ ⛔ 不靠"看输出像不像 PASS" · ⛔ 跑完**一定**还原(`finally`),异常路径也不许留下污染 """ if not tgt.exists(): print("⛔ 被检文件不存在:%s" % tgt) return 2 text = tgt.read_bytes().decode("utf-8", errors="replace") if old not in text: print("⛔ 锚点不在被检文件里 ⇒ **变异未生效** ⇒ 本轮作废(⛔ 不许输出通过)") print(" 锚点:%r" % old[:80]) return 2 orig = tgt.read_bytes() md5_0 = hashlib.md5(orig).hexdigest() n_red = 0 try: for k in range(cases): try: mutated = text.replace(old, new, 1) if mutated == text: print(" ⛔ 变异 %d 植入后文本**没变** ⇒ 等价变异,本轮作废" % (k + 1)) print(" ⛔ 变异必须动**语义**,⛔ 不能只动写法(今天栽过)") return 2 tgt.write_text(mutated, encoding="utf-8", newline="") if _md5(tgt) == md5_0: print(" ⛔ 变异 %d 写入后 md5 没变 ⇒ 没真改,本轮作废" % (k + 1)) return 2 r = subprocess.run([PY, str(SELFTEST), "-k", kw], capture_output=True, text=True, encoding="utf-8", errors="replace", cwd=str(tgt.parent.parent)) out = (r.stdout or "") + (r.stderr or "") red = bool(re.search(r"^\s*✗", out, re.M)) n_red += 1 if red else 0 print(" 变异 %d/%d:rc=%d |判据%s" % (k + 1, cases, r.returncode, "**报红 ✅**" if red else "⛔ 没报红 ❌")) if not red: for ln in out.splitlines(): if "合计" in ln or ln.strip().startswith("✗"): print(" %s" % ln.strip()[:110]) except Exception as e: print(" ⛔ 变异 %d 自身失败:%r ⛔ 本轮结论作废" % (k + 1, e)) return 2 finally: # 🔴 一定还原(⛔ 异常路径也还原 ⇒ 不留污染) tgt.write_bytes(orig) if _md5(tgt) != md5_0: print("⛔⛔ 还原后 md5 **不一致**(%s ≠ %s)⇒ 环境已污染,⛔ 后续结论全部作废" % (_md5(tgt)[:8], md5_0[:8])) return 2 print(" ✅ 还原核对:md5 一致(%s)" % md5_0[:8]) return 0 if (cases == n_red) else 1 def main() -> int: ap = argparse.ArgumentParser(description="判据元体检 · 防恒绿/假绿", allow_abbrev=False) ap.add_argument("--audit", nargs="?", default=str(SELFTEST)) ap.add_argument("--mutate", metavar="被检文件") ap.add_argument("rest", nargs="*", metavar="旧文本 新文本") ap.add_argument("-k", "--kw", default="") ap.add_argument("--cases", type=int, default=1) ap.add_argument("--list-rules", action="store_true") a = ap.parse_args() if a.list_rules: for name, kind, _p, why in RULES: print("%-20s [%s] %s" % (name, kind, why)) return 0 if a.mutate: # 🔴 2026-10-04 修:旧/新文本**走位置参数**。 # 原先用 `--old`/`--new`,而它们是**带前导空格的代码行** ⇒ shell 正常传、 # 但 `--old` 的值以空格开头时被 argparse 判成"unrecognized arguments" # ⇒ rc=2 来自**参数解析失败**,⛔ 不是"识别出等价变异" ⇒ 结论是假的 # (今天第二次栽在"验证器自己出错却输出结论")。 # ⇒ 现在:位置参数,且**参数个数不对就直接拒绝**,⛔ 不猜。 if len(a.rest) != 2: print("⛔ 需要**两个位置参数**:`旧文本` `新文本`(收到 %d 个)" % len(a.rest)) print(" 例:judge_audit.py --mutate <文件> ' return s.lower()' ' return s' -k <关键字>") return 2 return do_mutate(Path(a.mutate), a.rest[0], a.rest[1], a.kw or " ", a.cases) if a.rest: ap.print_help() return 2 return 0 if audit(Path(a.audit)) == 0 else 1 if __name__ == "__main__": sys.exit(main())