#!/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())