You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

88 lines
3.6 KiB

# -*- coding: utf-8 -*-
"""票 05:Bruno 文档完整性对账——契约 85 端点 vs E:\\code\\crm-api-docs .bru 覆盖。
用法: py -X utf8 bruno-audit-t05.py [--quiet]
输出: bruno-audit-t05.json(缺/余/私产/无钥匙/多副本全量)+ 控制台摘要。
"""
import json
import re
import sys
from pathlib import Path
SUMMARY = Path(r'e:\code\crm-backend-matt\.scratch\customer-module\API-SUMMARY.md')
DOCS = Path(r'E:\code\crm-api-docs')
OUT = Path(r'e:\code\crm-backend-matt\.scratch\customer-frontend-handover\bruno-audit-t05.json')
# ---------- 契约端点:API-SUMMARY 三种行形态并集 ----------
# a) 表格首列型:`| GET | `/api/xxx` |`
# b) 页签名列型:`| 公共头部 | GET `/api/xxx` |`(方法裸写 + 同格反引号路径)
# c) 内联型:`` `POST /api/xxx?id=` ``(正文行,方法+路径同段反引号)
ep_pats = [
re.compile(r'^\|\s*(GET|POST)\s*\|\s*`([^`]+)`\s*\|', re.M),
re.compile(r'\|\s*[^|]+\|\s*(GET|POST)\s+`([^`]+)`\s*\|', re.M),
re.compile(r'`(GET|POST)\s+(/api/[^`\uff08]+)`'),
]
seen, contract = set(), []
for pat in ep_pats:
for m in pat.finditer(SUMMARY.read_text(encoding='utf-8')):
path = m.group(2).split('?')[0].strip().rstrip('/')
if not path.startswith('/api/'):
continue
key = f'{m.group(1)} {path}'
if key not in seen:
seen.add(key)
contract.append(key)
# ---------- .bru 钥匙:docs 内 `METHOD /api/...`(对账钥匙行)----------
key_re = re.compile(r'`([A-Z]+)\s+(/api/[^`\uff08]+)`')
gen_re = re.compile(r'tags:\s*\[[^\]]*generated', re.S)
bru_keys, private, nogen = {}, [], []
for p in sorted(DOCS.rglob('*.bru')):
if p.name == 'collection.bru' or 'environments' in p.parts:
continue
txt = p.read_text(encoding='utf-8', errors='replace')
rel = str(p.relative_to(DOCS)).replace('\\', '/')
if not gen_re.search(txt):
private.append(rel) # 私产:不参与对账
continue
km = key_re.search(txt)
if not km:
nogen.append(rel) # 无法识别:不动,报告裁决
continue
bru_keys.setdefault(f'{km.group(1)} {km.group(2).split("?")[0].strip()}', []).append(rel)
# ---------- 三向对比 ----------
missing = [k for k in contract if k not in bru_keys]
covered = [k for k in contract if k in bru_keys]
cust_prefix = ('/api/customer', '/api/contact')
extra = sorted(k for k in bru_keys
if k not in seen
and any(k.split(' ', 1)[1].startswith(pfx) for pfx in cust_prefix))
dup = {k: v for k, v in bru_keys.items() if len(v) > 1}
report = {
'contract_count': len(contract),
'covered_count': len(covered),
'bru_generated_files': sum(len(v) for v in bru_keys.values()),
'bru_distinct_keys': len(bru_keys),
'missing': missing,
'extra_customer_domain': extra,
'private_files': private,
'no_key_files': nogen,
'multi_copy_keys': sorted(dup),
}
OUT.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding='utf-8')
brief = {k: (v if not isinstance(v, (list, dict)) or len(v) <= 8 else f'{len(v)} items')
for k, v in report.items()}
print(json.dumps(brief, ensure_ascii=False, indent=2))
if missing:
print('\n== MISSING(契约有 .bru 无)==')
print('\n'.join(' ' + k for k in missing))
if extra:
print('\n== EXTRA(客户域 .bru 有 契约无)==')
print('\n'.join(' ' + k for k in extra))
if nogen:
print('\n== 无法识别(无钥匙行)==')
print('\n'.join(' ' + k for k in nogen))
print('\nVERDICT:', 'PASS 零缺零余' if not missing and not extra and not nogen else 'FAIL 见上')