deliver: 交付收口(引擎代为提交)
This commit is contained in:
parent
cc20471188
commit
b3ad3f40cf
@ -150,8 +150,8 @@ INSERT —— `permission.permcode` = 路径本身,`rolepermission` 用 `role.
|
||||
|
||||
> **自检入口**:离线门禁自检脚本 `scripts/selfcheck_m5a.py` 已落盘(只读、不连库、
|
||||
> 输出逐字节确定),一条命令覆盖「三处同步 / RBAC 清单 vs 磁盘 / 模型四段式 /
|
||||
> 幂等键语义 / pyproject 依赖 / 种子 JSON / C-3 fail-closed / README 路径命中」8 项,
|
||||
> 全通过输出 `ALL PASS (8/8 checks)` 且 exit 0:
|
||||
> 幂等键语义 / pyproject 依赖 / 种子 JSON / C-3 fail-closed / README 路径命中」8 项 + 「harness 静态门禁 / harness 零明文凭据 / 运行证据日志 / harness DDL 与模型一致」4 项,共 12 项,
|
||||
> 全通过输出 `ALL PASS (12/12 checks)` 且 exit 0:
|
||||
>
|
||||
> ```bash
|
||||
> cd modules/pbl_evidence && python3 scripts/selfcheck_m5a.py
|
||||
@ -181,7 +181,7 @@ python3 scripts/check_crud_json.py
|
||||
# ④ RBAC 清单与 wwwroot 磁盘一致性核对(不连库)
|
||||
python3 scripts/load_path.py --check
|
||||
|
||||
# ⑤ M5a 交付门禁一键自检(8 项全 PASS 才算可交付;失败 exit 1 并逐条打印原因)
|
||||
# ⑤ M5a 交付门禁一键自检(12 项全 PASS 才算可交付;失败 exit 1 并逐条打印原因)
|
||||
python3 scripts/selfcheck_m5a.py
|
||||
```
|
||||
|
||||
@ -253,7 +253,7 @@ pbl_evidence/
|
||||
├── init/data.json # 编码种子(Format B:3 组 appcodes + appcodes_kv 子项)
|
||||
├── sql/pbl_evidence.sql # 建表 SQL 快照
|
||||
├── scripts/load_path.py # RBAC 登记(模块层,禁通配符)+ --check 一致性核对
|
||||
├── scripts/selfcheck_m5a.py # M5a 交付门禁自检(8 项,只读不连库,ALL PASS 收尾)
|
||||
├── scripts/selfcheck_m5a.py # M5a 交付门禁自检(12 项,只读不连库,ALL PASS 收尾)
|
||||
├── skill/SKILL.md # 面向 agent 的模块技能文档
|
||||
└── pyproject.toml
|
||||
```
|
||||
|
||||
286
scripts/verify_m5a_closure.py
Normal file
286
scripts/verify_m5a_closure.py
Normal file
@ -0,0 +1,286 @@
|
||||
#!/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())
|
||||
Loading…
x
Reference in New Issue
Block a user