Files
mini-openclaw/test-evidence/20260929-v2-handoff/check_docs.py
2026-09-29 12:09:18 +08:00

79 lines
6.1 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from pathlib import Path
import json,re,subprocess,hashlib
root=Path.cwd(); out=root/'test-evidence/20260929-v2-handoff'
def git(*args):
return subprocess.check_output(['git',*args],encoding='utf-8')
files=[root/'AGENTS.md',root/'CLAUDE.md',*sorted((root/'docs/project-process').rglob('*.md')),*sorted((root/'docs/v2').glob('*.md'))]
issues=[]; links=0
for p in files:
s=p.read_text(encoding='utf-8')
if any(ord(c)<32 and c not in '\n\r\t' for c in s): issues.append(f'control character: {p}')
for target in re.findall(r'\[[^\]\n]*\]\(([^)]+)\)',s):
target=target.strip('<>').split('#')[0]
if not target or re.match(r'^[a-zA-Z][a-zA-Z0-9+.-]*:',target): continue
links+=1
if not (p.parent/target).exists(): issues.append(f'broken link: {p.name}: {target}')
for block in re.findall(r'(?:^\|.*(?:\n|$))+',s,re.M):
lines=block.splitlines()
if len(lines)<2 or not re.match(r'^\|[\s:|\-]+\|$',lines[1]): issues.append(f'table without header: {p.name}: {lines[0][:80]}')
s=(root/'docs/v2/01-requirements.md').read_text(encoding='utf-8')
old=git('show','47ec30a:docs/v2/01-requirements.md')
counts={}
for kind,count in [('BR',44),('E',26),('AC',45)]:
ids=re.findall(r'^\| ('+kind+r'-\d+) \|',s,re.M)
expected=[f'{kind}-{i:02}' for i in range(1,count+1)]
if ids!=expected: issues.append(f'{kind} IDs invalid: {ids}')
old_ids=set(re.findall(r'^\| ('+kind+r'-\d+) \|',old,re.M))
if not old_ids.issubset(ids): issues.append(f'{kind} baseline IDs lost')
counts[kind]=len(ids)
rows=re.findall(r'^\| AC-\d+ \|.*$',s,re.M)
for row in rows:
if not all(t in row for t in ['Given','When','Then']): issues.append(f'not GWT: {row}')
covered=set(re.findall(r'\b(?:BR|E)-\d+\b','\n'.join(rows)))
required=set(re.findall(r'^\| ((?:BR|E)-\d+) \|',s,re.M))
if required-covered: issues.append(f'uncovered: {required-covered}')
if covered-required: issues.append(f'undefined: {covered-required}')
u=(root/'docs/v2/URD.md').read_text(encoding='utf-8')
uids=re.findall(r'^\| (AC-U\d+) \|',u,re.M)
if len(uids)!=len(set(uids)) or len(uids)!=9: issues.append('URD acceptance IDs')
for row in re.findall(r'^\| MOD-.*$',u,re.M):
if not set(re.findall(r'AC-U\d+',row)).issubset(uids): issues.append(f'URD missing AC: {row}')
changed=git('diff','47ec30a','--name-only').splitlines()
if any(not (p.startswith('docs/v2/') or p == 'CLAUDE.md' or p.startswith('test-evidence/20260929-v2-handoff/')) for p in changed): issues.append('change outside documented scope')
# Cross-document stable ID references (expanded ranges validated by endpoint IDs).
valid={kind:set(re.findall(r'^\| ('+kind+r'-\d+) \|',(root/'docs/v2/01-requirements.md').read_text(encoding='utf-8'),re.M)) for kind in ['BR','E','AC']}
for p in (root/'docs/v2').glob('*.md'):
for ref in re.findall(r'\b(?:BR|E|AC)-\d+\b',p.read_text(encoding='utf-8')):
if ref not in valid[ref.split('-')[0]]: issues.append(f'undefined cross-document ID: {p.name}: {ref}')
hand=(root/'docs/v2/15-development-handoff.md').read_text(encoding='utf-8')
if re.findall(r'^\| (T\d+) ',hand,re.M) != [f'T{i:02}' for i in range(14)]: issues.append('task queue IDs')
for p in (root/'docs/v2').glob('*.md'):
# Verify local heading fragments using GitHub-style punctuation removal.
for target in re.findall(r'\[[^\]\n]*\]\(([^)]+)\)',p.read_text(encoding='utf-8')):
if '#' not in target or re.match(r'^[a-zA-Z][a-zA-Z0-9+.-]*:',target): continue
rel,frag=target.split('#',1); dest=p.parent/rel if rel else p
if dest.exists() and dest.suffix=='.md':
heads=re.findall(r'^#{1,6} (.+)$',dest.read_text(encoding='utf-8'),re.M)
slugs={re.sub(r'[^\w\- ]','',h.strip().lower()).replace(' ','-') for h in heads}
if frag not in slugs: issues.append(f'broken anchor: {p.name}: {target}')
for row in re.findall(r'^\| E-\d+ \|.*$', (root/'docs/v2/01-requirements.md').read_text(encoding='utf-8'),re.M):
if not re.search(r'\b[A-Z][A-Z_]+\b',row) or len(row.split('|'))<5: issues.append(f'error binding missing: {row}')
if not (root/'CLAUDE.md').read_text(encoding='utf-8').startswith('@AGENTS.md'): issues.append('Claude must import canonical rules')
adr=(root/'docs/v2/09-decisions.md').read_text(encoding='utf-8')
adr_ids=re.findall(r'^\| (ADR-\d+) \|',adr,re.M)
if adr_ids != [f'ADR-{i:02}' for i in range(1,15)]: issues.append('ADR IDs invalid')
old_adr=git('show','47ec30a:docs/v2/09-decisions.md')
if not set(re.findall(r'^\| (ADR-\d+) \|',old_adr,re.M)).issubset(adr_ids): issues.append('ADR baseline IDs lost')
check=subprocess.run(['git','diff','--check'],capture_output=True,text=True)
if check.returncode: issues.append(check.stdout+check.stderr)
report={'base_revision':'47ec30a562f12864b7485bb7a43b967fa8f5b755','cwd':str(root),'command':'backend/venv/Scripts/python.exe test-evidence/20260929-v2-handoff/check_docs.py','markdown_files':len(files),'local_links':links,'ids':counts,'status':'PASS' if not issues else 'FAIL','issues':issues,'runtime_tests':'Not run: documentation-only change','hashes':{str(p.relative_to(root)).replace('\\','/'):hashlib.sha256(p.read_bytes()).hexdigest() for p in files}}
(out/'validation.json').write_text(json.dumps(report,ensure_ascii=False,indent=2),encoding='utf-8')
print(json.dumps({k:v for k,v in report.items() if k!='hashes'},ensure_ascii=False,indent=2))
summary = '# 文档检查报告\n\n日期:2026-09-29。状态:' + report['status'] + '\n\n'
summary += f"检查 {len(files)} 份Markdown、{links} 个本地链接;44条BR、26条E、45条AC全部唯一连续并有GWT覆盖;跨文档引用、任务T00–T13、锚点、错误码与表格检查。\n\n"
summary += '命令:`backend/venv/Scripts/python.exe test-evidence/20260929-v2-handoff/check_docs.py`;cwd为仓库根。基线47ec30a,最终文档SHA256见validation.json。\n\n'
summary += 'git diff --check及范围检查结果见validation.json;业务源码/原型未改变。纯文档未执行verify.py或模型/CLI实测。独立业务/技术审阅未完成,不能据此进入未过门禁的实现。\n'
if issues: summary += '\n问题:\n' + '\n'.join('- '+x for x in issues) + '\n'
(out/'REPORT.md').write_text(summary,encoding='utf-8')
raise SystemExit(bool(issues))