# -*- 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 见上')