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.
89 lines
3.6 KiB
89 lines
3.6 KiB
|
1 day ago
|
# -*- 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 见上')
|