- 按用户指示清空原有 25 技能内容,只提交 session-mechanism(57 文件) - 附 .gitignore(产物 + 本机凭据) - 令牌明文已脱敏(历史 .neodata_token 与 pitfalls 引用均不入库) - 本提交为孤儿提交(父提交为空),历史自此重新开始
263 lines
13 KiB
Python
263 lines
13 KiB
Python
# -*- 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())
|