#!/usr/bin/env python3 # -*- coding: utf-8 -*- """audit_active_state.py — 根目录 active.md 体量闸(doc-conventions §10 的机械面) 状态文件只记当下:条目只增不减就是腐烂。本脚本查五条机械可判的膨胀签名—— 状态记录条目数超上限、快照节行数超上限、条目编号重复、快照整行自我重复、 指向中间文档(design/qa、design/research)的死指针。 退出码:0=全绿;1=有红(逐条打印差额,供收口批据此删条目)。 用法:python tools/audit_active_state.py [--file PATH] [--quiet] """ import argparse import os import re import sys ENTRY_LIMIT = 30 # §10:状态记录超过 30 条必须清 SNAPSHOT_LIMIT = 10 # §10:当前状态快照 ≤10 行 INTERMEDIATE_PREFIX = ("design/qa/", "design/research/") ENTRY_RE = re.compile(r"^- \*\*(\d+)( 续)?") BULLET_RE = re.compile(r"^- .{20,}") PATH_RE = re.compile(r"design/(?:qa|research)/[^\s)`,,。)]+?\.md") def split_sections(text): """按二级标题切段;标题前的内容归入 '' 键(文件头元信息,本闸不判)。""" out, key = {}, "" for line in text.splitlines(): if line.startswith("## "): key = line[3:].strip() out[key] = [] continue out.setdefault(key, []).append(line) return out def find_key(sections, prefix): for name in sections: if name.startswith(prefix): return name return None def audit(path, repo): with open(path, encoding="utf-8") as f: sections = split_sections(f.read()) reds = [] snap_key = find_key(sections, "当前状态快照") rec_key = find_key(sections, "状态记录") if snap_key is None: reds.append("A0 快照节缺失:状态文件必须含「当前状态快照」节") if rec_key is None: reds.append("A0 状态记录节缺失:状态文件必须含「状态记录」节") snap = [ln for ln in sections.get(snap_key, []) if BULLET_RE.match(ln)] if len(snap) > SNAPSHOT_LIMIT: reds.append("A1 快照节 %d 条,超上限 %d——先删被新一轮取代的旧行,别只往顶部加" % (len(snap), SNAPSHOT_LIMIT)) seen = {} for ln in snap: seen[ln] = seen.get(ln, 0) + 1 dups = [ln for ln, n in seen.items() if n > 1] if dups: reds.append("A2 快照节有 %d 行整行自我重复(换代不彻底的签名):%s" % (len(dups), dups[0][:60] + " …")) nums, conts = [], [] for ln in sections.get(rec_key, []): m = ENTRY_RE.match(ln) if m: (conts if m.group(2) else nums).append(int(m.group(1))) if len(nums) > ENTRY_LIMIT: reds.append("A3 状态记录 %d 条,超硬上限 %d——按 §10 判据删除已完结批次条目" % (len(nums), ENTRY_LIMIT)) repeated = sorted({n for n in nums if nums.count(n) > 1}) if repeated: reds.append("A4 条目编号重复(并行侧写撞号):%s" % "、".join(str(n) for n in repeated)) body = "\n".join(sections.get(snap_key, []) + sections.get(rec_key, []) + sections.get(find_key(sections, "未决面"), [])) dead = sorted({p for p in PATH_RE.findall(body) if not os.path.exists(os.path.join(repo, p.replace("`", "")))}) if dead: reds.append("A5 指向中间文档的死指针 %d 处(文件已按 §11 退役,条目却还在引):%s" % (len(dead), "、".join(dead))) return reds, len(nums), len(snap) def main(): ap = argparse.ArgumentParser(description="active.md 体量闸") ap.add_argument("--file", default=None, help="待审文件(默认为本仓库根目录 active.md)") ap.add_argument("--quiet", action="store_true", help="只输出结论行") args = ap.parse_args() repo = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) path = args.file or os.path.join(repo, "active.md") if not os.path.exists(path): print("[active.md 体量闸] 找不到文件:%s" % path) return 1 reds, entries, snap = audit(path, repo) if reds: print("[active.md 体量闸] FAIL:%d 项——状态记录 %d 条 / 快照 %d 行" % (len(reds), entries, snap)) for r in reds: print(" " + r) return 1 if not args.quiet: print("[active.md 体量闸] PASS:状态记录 %d 条 / 快照 %d 行,编号唯一、指针全活" % (entries, snap)) return 0 if __name__ == "__main__": try: sys.stderr.reconfigure(encoding="utf-8", errors="replace") sys.stdout.reconfigure(encoding="utf-8", errors="replace") except Exception: pass sys.exit(main())