feat(收口闸): active.md 体量闸并入全量闸清单——收口漏删旧条目在合回时被拦

- tools/audit_active_state.py:判四条可机械测的膨胀签名(状态记录条目数>30、
  快照行数>10、条目编号重复含并行撞号、指向 design/qa|research 中间文档的死指针),
  另加快照整行自我重复一项(换代不彻底只往顶部 prepend 的签名)。
- 变异验证:对换代前的 34 条版本报 5 项红(A1/A2/A3/A4/A5 全见红),
  对退役后的 3 条版本报绿;两态实测 RC=1/0。
- 入 run_all_gates.sh 为 08b(脚本即清单,不另立第二份闸清单),baseline.json
  随批补 08b 绿项(本分支内实测绿,非臆造)。
- 规约挂指针两处:doc-conventions §10 机械闸句、process-conventions 批次清单 12。
- active.md 按 §10 退役:状态记录 34→3 条,快照 21 行自我重复压回 10 行,
  删「项目当前位置」整节(滞后且与 roadmap/快照重复),未决面按迭代 27/28 重列。
- 不动 Stop 钩子:回合末判收口时机错配(一轮答完≠任务完成),机械层交合回闸,
  语义层交收口技能。
This commit is contained in:
DKChan committed 2026-10-15 22:23:53 +08:00
1 parent 458085ed32
commit 20f4a7dc01
6 files changed
+148 -97

No files matched your search

+120
View File
@@ -0,0 +1,120 @@
#!/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())
+2
View File
@@ -111,6 +111,8 @@ fi
gate "06 gdd_lint" "gdd_lint.log" "$PY" "$REPO/tools/gdd_lint.py"
gate "07 doc_link" "doc_link.log" "$PY" "$REPO/tools/doc_link_audit.py" --strict
gate "08 doc_term" "doc_term.log" "$PY" "$REPO/tools/doc_term_audit.py"
# ── 6b. 状态文件体量闸(active.md 只记现在:条目数/快照行数/编号/死指针四条硬上限)──
gate "08b active.md 体量" "active_state.log" "$PY" "$REPO/tools/audit_active_state.py"
# ── 7. 静态扫描域 + 熵增闸 ──
if [ $QUICK -eq 0 ]; then
gate "09 静态扫描" "static_scan.log" bash "$GAME/tests/static_scan.sh"