pbl_evidence/scripts/verify_m5a_closure.py
2026-09-22 20:45:25 +08:00

287 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""M5a 收口自证脚本(QC #1/#7/#8 退回项的**仓库内正式**取证工具)。
为什么存在
----------
上一轮把「新代码是否真在磁盘、证据日志是否齐备、临时补丁是否已清理」这套自证逻辑
写在工作空间根的 `tmp_patch_u7rb.py` 里,以一次性工具脚本充当 develop 交付件本体
(QC #8)。一次性脚本不入库 = 下一个人无从复跑,自证结论无法复核。本脚本把同一套
检查沉淀进模块仓库,与 `selfcheck_m5a.py` 互补:
* `selfcheck_m5a.py` —— 交付**门禁**(12 项,验模块结构/契约/幂等键/证据日志口径)
* 本脚本 —— **收口自证**(验「磁盘代码 == 门禁所声称的那份代码」+ 工作空间卫生 +
README 与代码的门禁条数一致,防文档/代码漂移)
用法
----
cd modules/pbl_evidence && python3 scripts/verify_m5a_closure.py
# 全通过 → 逐条 [PASS] + `CLOSURE VERIFY ALL PASS (N/N)`,exit 0
# 任一失败 → [FAIL] + 原因,exit 1
幂等契约:只读(不连库、不写文件、不取时间戳/随机数/绝对路径),所有明细 sorted()
后输出,重复执行逐字节一致。
"""
import ast
import json
import os
import re
import sys
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
SELFCHK = os.path.join(ROOT, 'scripts', 'selfcheck_m5a.py')
HARNESS = os.path.join(ROOT, 'tests', 'm5a_live_db_harness.py')
README = os.path.join(ROOT, 'README.md')
# 机构工作空间根:modules/{模块}/scripts/ → up 3 级
WS_ROOT = os.path.abspath(os.path.join(ROOT, os.pardir, os.pardir))
DOCS = os.path.join(WS_ROOT, 'projects', 'pbls', 'docs', '02-develop')
ENV_TEST = os.path.join(WS_ROOT, 'projects', 'pbls', 'env', 'test.json')
# ① selfcheck 必须定义的 4 个新增门禁函数(QC #1 点名的改法)
GATE_FUNCS = (
'check_harness_static',
'check_no_hardcoded_credentials',
'check_evidence_logs',
'check_harness_matches_model',
)
# ② harness 里必须 0 命中的崩溃/吞参/明文凭据特征(QC #1/#2 点名的旧缺陷)
HARNESS_FORBIDDEN = ('test123', 'args or {}', 'params or {}', 'dict(params or {})')
# ③ 三份运行证据日志(与 selfcheck EVIDENCE_LOGS 同名单)
EVIDENCE_LOGS = ('m5a-live-db-harness.log', 'm5a-selfcheck.log', 'm5a-pytest-offline.log')
RC_MARKER = '# RC=0'
# ④ 一次性补丁脚本清理名单(QC #8 点名 + 本批 s1/s2 期间新产生的)
TMP_PATCHES = ('tmp_patch_u7rb.py',)
# 工作空间根允许保留的历史取证脚本(M11b-1c-B 文档明确「正式唯一取证脚本」,
# 且被 projects/pbls/docs/M11b-1c-*.md 逐条引用作复跑入口,删除会破坏历史验收可复现性)
TMP_KEPT = ('tmp_m11b1c_diff_check.py',)
def _read(path):
with open(path, 'r', encoding='utf-8') as fh:
return fh.read()
def _rel(path):
try:
return os.path.relpath(path, WS_ROOT).replace(os.sep, '/')
except Exception:
return os.path.basename(path)
# ── V1 selfcheck 四个门禁函数已落盘且被 CHECKS 装配(防「定义了但不可达」死代码)
def verify_gate_functions():
if not os.path.isfile(SELFCHK):
return False, '缺少 %s' % _rel(SELFCHK)
src = _read(SELFCHK)
try:
tree = ast.parse(src, filename=SELFCHK)
except Exception as exc:
return False, 'selfcheck_m5a.py 语法错误: %s' % exc
defined = {n.name for n in tree.body
if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))}
problems, detail = [], []
missing = sorted(f for f in GATE_FUNCS if f not in defined)
if missing:
problems.append('selfcheck 缺门禁函数: %s' % ', '.join(missing))
else:
detail.append('4 个门禁函数已定义: %s' % ', '.join(sorted(GATE_FUNCS)))
# 装配检查:函数名必须出现在 CHECKS 列表里,否则是永远不执行的死代码
m = re.search(r'^CHECKS\s*=\s*\[(.*?)^\]', src, re.S | re.M)
if not m:
problems.append('未找到 CHECKS 装配表')
else:
unwired = sorted(f for f in GATE_FUNCS
if not re.search(r'\b' + re.escape(f) + r'\b(?!\s*def)', m.group(1)))
if unwired:
problems.append('门禁函数未装配进 CHECKS(死代码): %s' % ', '.join(unwired))
else:
detail.append('均已装配进 CHECKS 装配表')
if problems:
return False, '; '.join(problems)
return True, ' | '.join(detail)
# ── V2 harness 修复到位:崩溃版特征 0 命中
def verify_harness_clean():
if not os.path.isfile(HARNESS):
return False, '缺少 %s' % _rel(HARNESS)
src = _read(HARNESS)
hits = []
for token in HARNESS_FORBIDDEN:
n = src.count(token)
if n:
hits.append('%s×%d' % (token, n))
if hits:
return False, 'harness 仍含崩溃/吞参/明文凭据特征: %s' % ', '.join(sorted(hits))
try:
ast.parse(src, filename=HARNESS)
except Exception as exc:
return False, 'harness 语法错误: %s' % exc
return True, ('%s 零命中 %s,且 ast.parse 通过(修复版已在磁盘)'
% (_rel(HARNESS), ' / '.join(HARNESS_FORBIDDEN)))
# ── V3 env/test.json 的 db.sandbox 段(凭据唯一事实源,代码内零明文)
def verify_env_sandbox():
if not os.path.isfile(ENV_TEST):
return False, '缺少 %s' % _rel(ENV_TEST)
try:
conf = json.loads(_read(ENV_TEST))
except Exception as exc:
return False, 'env/test.json 不是合法 JSON: %s' % exc
db = conf.get('db') or {}
sb = db.get('sandbox')
if not isinstance(sb, dict):
return False, 'env/test.json 缺 db.sandbox 段(harness 无凭据事实源)'
problems, detail = [], []
if sb.get('scope') != 'sandbox_only':
problems.append('db.sandbox.scope != sandbox_only(会被误用于业务库)')
else:
detail.append('scope=sandbox_only')
if not sb.get('sandbox_schema'):
problems.append('db.sandbox 缺 sandbox_schema(沙箱库名未声明)')
else:
detail.append('sandbox_schema=%s' % sb['sandbox_schema'])
# 沙箱库由 harness 运行时 CREATE DATABASE / DROP DATABASE,故不预置 dbname;
# 连接必填四项 + 自建 schema 名,才是这段配置的真实语义。
for key in ('host', 'port', 'user', 'password'):
if key not in sb:
problems.append('db.sandbox 缺连接字段 %s' % key)
if problems:
return False, '; '.join(problems)
detail.append('连接必填字段齐备(host/port/user/password),schema 由 harness 自建自删')
return True, ' | '.join(detail)
# ── V4 三份证据日志齐备,且 harness 日志含 C-3 fail-closed + 1062 真防重原文
def verify_evidence_logs():
if not os.path.isdir(DOCS):
return False, '缺少证据目录 %s' % _rel(DOCS)
top = sorted(f for f in os.listdir(DOCS) if os.path.isfile(os.path.join(DOCS, f)))
problems, detail = [], []
absent = sorted(set(EVIDENCE_LOGS) - set(top))
if absent:
problems.append('缺少证据日志: %s' % ', '.join(absent))
else:
detail.append('三份日志齐备: %s' % ', '.join(EVIDENCE_LOGS))
for name in EVIDENCE_LOGS:
if name not in top:
continue
text = _read(os.path.join(DOCS, name))
if name == 'm5a-selfcheck.log':
n = len(re.findall(r'^\[(?:PASS|FAIL)\] \d+\.', text, re.M))
if n != 12:
problems.append('m5a-selfcheck.log [PASS]/[FAIL] 行数 %d != 12' % n)
elif 'ALL PASS (12/12 checks)' not in text:
problems.append('m5a-selfcheck.log 结论行不是 ALL PASS (12/12 checks)')
else:
detail.append('自检日志 12 行齐备且结论为 ALL PASS (12/12 checks)')
elif RC_MARKER not in text:
problems.append('%s 缺 `%s` 收尾标记' % (name, RC_MARKER))
if re.search(r'^Traceback \(most recent call last\)', text, re.M):
problems.append('%s 含裸 Traceback 崩溃现场' % name)
if not problems and 'm5a-live-db-harness.log' in top:
hlog = _read(os.path.join(DOCS, 'm5a-live-db-harness.log'))
need = ['C-3.1', 'C-3.2', 'C-3.3', 'C-3.4', 'IDM.6']
miss = sorted(t for t in need
if not re.search(r'\[PASS\]\s*' + re.escape(t) + r'\b', hlog))
if miss:
problems.append('harness 日志缺 [PASS] 行: %s' % ', '.join(miss))
elif '1062' not in hlog or 'uk_ev_dedup' not in hlog:
problems.append('harness 日志缺 1062 Duplicate entry / uk_ev_dedup 真防重原文')
else:
detail.append('harness 日志含 C-3.1~C-3.4 全 [PASS] + IDM.6 1062(uk_ev_dedup) 原文')
if problems:
return False, '; '.join(problems)
return True, ' | '.join(detail)
# ── V5 工作空间卫生:QC #8 点名的一次性补丁已删,历史取证脚本按文档保留
def verify_tmp_hygiene():
problems, detail = [], []
still = sorted(f for f in TMP_PATCHES if os.path.isfile(os.path.join(WS_ROOT, f)))
if still:
problems.append('一次性补丁脚本仍在磁盘: %s' % ', '.join(still))
else:
detail.append('QC #8 点名脚本已删除: %s' % ', '.join(TMP_PATCHES))
left = sorted(f for f in os.listdir(WS_ROOT)
if f.startswith('tmp_') and f.endswith('.py'))
unexpected = sorted(set(left) - set(TMP_KEPT))
if unexpected:
problems.append('工作空间根仍有未清理的一次性补丁: %s' % ', '.join(unexpected))
kept = sorted(set(left) & set(TMP_KEPT))
if kept:
detail.append('按 M11b-1c-B 文档保留历史取证脚本: %s' % ', '.join(kept))
if problems:
return False, '; '.join(problems)
return True, ' | '.join(detail)
# ── V6 README 门禁条数与代码实际条数一致(防文档/代码漂移)
def verify_readme_matches_code():
problems, detail = [], []
if not os.path.isfile(README):
return False, '缺少 README.md'
if not os.path.isfile(SELFCHK):
return False, '缺少 scripts/selfcheck_m5a.py'
rtxt = _read(README)
stxt = _read(SELFCHK)
try:
tree = ast.parse(stxt, filename=SELFCHK)
except Exception as exc:
return False, 'selfcheck 语法错误: %s' % exc
n_checks = None
for node in tree.body:
if isinstance(node, ast.Assign) and any(
getattr(t, 'id', None) == 'CHECKS' for t in node.targets):
if isinstance(node.value, ast.List):
n_checks = len(node.value.elts)
if n_checks is None:
return False, '无法从 selfcheck 解析 CHECKS 条数'
claimed = sorted(set(re.findall(r'ALL PASS \((\d+)/\d+ checks\)', rtxt)))
if claimed != [str(n_checks)]:
problems.append('README 声称的门禁条数 %s 与代码 CHECKS 条数 %d 不一致'
% (claimed, n_checks))
else:
detail.append('README 门禁条数 = 代码 CHECKS 条数 = %d' % n_checks)
if problems:
return False, '; '.join(problems)
return True, ' | '.join(detail)
CHECKS = [
('selfcheck 四个新增门禁函数已落盘且装配进 CHECKS', verify_gate_functions),
('harness 修复版在磁盘:test123/args or {} 等 0 命中 + ast 可解析', verify_harness_clean),
('env/test.json db.sandbox 段齐备(scope=sandbox_only,凭据唯一事实源)', verify_env_sandbox),
('三份运行证据日志齐备(C-3.1~C-3.4 + 1062 真防重原文 + # RC=0)', verify_evidence_logs),
('工作空间根一次性补丁脚本已清理(QC #8)', verify_tmp_hygiene),
('README 门禁条数与 selfcheck CHECKS 条数一致', verify_readme_matches_code),
]
def main():
print('verify_m5a_closure: target = modules/pbl_evidence')
failed = []
for i, (name, fn) in enumerate(CHECKS, 1):
try:
ok, detail = fn()
except Exception as exc:
ok, detail = False, '检查执行异常 %s: %s' % (type(exc).__name__, exc)
print('[%s] V%d. %s' % ('PASS' if ok else 'FAIL', i, name))
print(' %s' % detail)
if not ok:
failed.append(name)
if failed:
print('CLOSURE VERIFY FAILED (%d/%d)' % (len(failed), len(CHECKS)))
for n in failed:
print(' - %s' % n)
return 1
print('CLOSURE VERIFY ALL PASS (%d/%d)' % (len(CHECKS), len(CHECKS)))
return 0
if __name__ == '__main__':
sys.exit(main())