判据(对 869 个 ZCode 会话与 16 个 Qoder 会话的读写统计):active.md 被整文件读 448 次、 单次中位数 14 KB、累计 6.09 MB 进上下文,311/320 个碰它的会话把它当第一个工具调用读; 但 7311 个提及它的思考片段的语境分布是——写入与编号簿记 327 个会话、并行冲突与避让 234 个、 退役与失真核对 162 个,而明确写「从它所载内容得知某事实」的只有 20 个会话。 逐条核 41 个条目:独特且现行 2 条(另 3 个子句)、别处已有权威源 14 条、陈旧或自相矛盾 7 条、 已完结批次的历史叙述 18 条;按字节约 95% 属后三类。失实样本:architecture 版本记 v1.6 实为 v1.8、 规则文件数记 7 份实为 6 份、迭代 27/28 记「位预留追加卡」而 roadmap 记已完成、三个已删 worktree 仍记「在盘待删」、同一条既宣称「文档禁列闸清单」又自行列出 01–11 全套闸。 执法件已建一轮仍拦不住:08b 体量闸只测条目数/行数/编号/死链,测不出语义失真;钩子不随仓库传播, 只有 ZCode 在跑;10-15 清到 8.6 KB、10-17 回到 40 KB,用户为「只写不删」连续三次发火。 改动面: - 删 active.md 与 tools/audit_active_state.py;run_all_gates.sh 摘除 08b 闸件与头注、 baseline.json 删 08b 记录(现存 12 闸)、repo_structure_audit.py 根文件白名单去 active.md - doc-conventions §10 改写为「仓库根不设状态文件」并留裁决与判据(保持 §11/§12 编号不漂); §11 表两行去状态文件口径;§4 适用范围去 active.md - process-conventions §1 第 2/3 条(合回前查尾号防撞号、豁免面)、§3 第 12 条(批次完成后的 登记义务)、§6 分级周期口径(闸件清单不再在文档重列)四处换代 - code-conventions「实现批收尾双登记」改为单登记;CLAUDE.md 目录节、规范导航行、worktree 豁免 行三处换代,红线新增「仓库根禁止设会话状态文件」一条 - roadmap.md 顶部新增「当前位」三行(阶段/迭代位/下一步)接替原快照职责,头注、执行纪律第 3 条、 loop 规范第 8 条、维护纪律四处改指 06 与本文;reports/README.md 与 sim-data/README.md 两处 现行指路改口;architecture.md 文档域闸表行去 active.md 体量闸与触发路径 - 06-待办清单迁入七条真残值(原仅载于该文件):卡片入库纪律悬空待办、req-card 两副本取齐、 魔法道具与罕见残留(实数 23/17 处,旧记约 30/18)、通关口径统一(gdd 内 188 处,旧记 273)、 黑话第二波待发令、数据整改批九条存疑待抽查(该批未提交面所载)、发布前删除 main.gd:196 调试 入口写死行(工程直入·待写卡) harness 侧另批处理(.zcode/ 与用户级技能不入 git):session-snapshot.sh 与 config.json 注册、 worktree-guard.sh 的 active.md 豁免白名单、active-closing 技能、doc-consistency-check 第 8/9 项、 iter 与 req-card 技能里的「唯一例外=active.md」与查尾号条款。
106 lines
5.2 KiB
Python
106 lines
5.2 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""仓库结构审计闸(熵增闸,2026-09-27 用户立项)。
|
||
|
||
收口第四闸(与 doc_link_audit / doc_term_audit / gdd_lint 同列):
|
||
把目录三层裁定变成机器检查——任何会话乱放文件,收口时此处直接 FAIL。
|
||
|
||
四查:
|
||
1. 顶层目录白名单(新增顶层目录 = FAIL,须同批改 CLAUDE.md + 本脚本白名单)
|
||
2. 根目录文件白名单(根上只许运行文件与规则文件)
|
||
3. 图像白名单(图只许在 artwork/ game/assets/ design/art/references/ design/dev/architecture/)
|
||
4. 退役路径守卫(production/ docs/ tests/ qa-data/ candidates/mockups 旧路径再现 = FAIL)
|
||
|
||
用法: python tools/repo_structure_audit.py # 全查,FAIL 时 exit 1
|
||
python tools/repo_structure_audit.py --json # 机器读输出
|
||
依据: CLAUDE.md「Project Layout」节(2026-09-27 目录三层收敛终态)。
|
||
"""
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
ROOT = Path(__file__).resolve().parent.parent
|
||
|
||
# 1) 顶层目录白名单(点开头目录一律忽略)
|
||
# reports/ 收编(2026-10-17 用户裁「迭代批禁写 design/」):迭代实现报告/代拍板追认单/GDD 反哺点
|
||
# 清单的家——实现批只准往这里写,design/ 全目录对它只读。只放 .md,不放图(图像白名单不含此目录)。
|
||
TOP_DIRS = {"archive", "artwork", "design", "game", "sim-data", "tools", "reports"}
|
||
# 2) 根目录文件白名单
|
||
ROOT_FILES = {"CLAUDE.md", "roadmap.md", "stage.txt", "review-mode.txt",
|
||
".gitignore", ".zcodeignore"}
|
||
# 3) 图像允许目录(相对路径前缀;archive/ 为归档 zip 不扫)
|
||
# design/qa/ 收编(迭代 5 返工批 2026-09-29——U2 目检预览图/QA 呈现产物归位,commit 留痕)
|
||
# game/tests/golden/ 收编(迭代 19 卡① T2/T6 2026-10-08——棋盘 golden 基线=机检测试资产非美术素材,
|
||
# 素材齐盘态生成入库【基线换代权限 Q6】;T1 落库时漏改本白名单致熵增闸红,同批修订 commit 留痕)
|
||
IMAGE_EXTS = {".png", ".jpg", ".jpeg", ".webp", ".gif", ".svg", ".pxo", ".psd", ".bmp"}
|
||
IMAGE_PREFIXES = ("artwork/", "game/assets/", "design/art/references/", "design/dev/architecture/",
|
||
"design/qa/", "game/tests/golden/")
|
||
# 4) 退役路径(目录或文件任一再现即 FAIL)
|
||
RETIRED = ["production", "docs", "tests", "qa-data",
|
||
"design/art/candidates", "production/mockups",
|
||
"design/ux", "game/tools"]
|
||
# design/ 一级子目录白名单(防 ad-hoc 新目录)
|
||
DESIGN_SUBDIRS = {"art", "dev", "gdd", "qa", "registry", "research"}
|
||
|
||
|
||
def scan():
|
||
fails, notes = [], []
|
||
|
||
# 1) 顶层目录
|
||
top_dirs = {p.name for p in ROOT.iterdir() if p.is_dir() and not p.name.startswith(".")}
|
||
extra = top_dirs - TOP_DIRS
|
||
if extra:
|
||
fails.append("顶层目录越界: %s(新增顶层目录须同批改 CLAUDE.md Project Layout + 本脚本 TOP_DIRS)" % sorted(extra))
|
||
|
||
# 2) 根目录文件(点开头忽略:.gitignore/.zcodeignore 白名单收编,.git 指针等本就不可见)
|
||
top_files = {p.name for p in ROOT.iterdir() if p.is_file() and not p.name.startswith(".")}
|
||
extra_f = top_files - {f for f in ROOT_FILES if not f.startswith(".")}
|
||
if extra_f:
|
||
fails.append("根目录文件越界: %s(运行文件与规则文件之外不放根上;新类别须同批改两处白名单)" % sorted(extra_f))
|
||
|
||
# 3) 图像白名单
|
||
stray = []
|
||
for ext in IMAGE_EXTS:
|
||
for p in ROOT.rglob("*" + ext):
|
||
rel = p.relative_to(ROOT).as_posix()
|
||
if rel.startswith((".git", ".zcode", ".qoder", "archive/")):
|
||
continue
|
||
if not rel.startswith(IMAGE_PREFIXES):
|
||
stray.append(rel)
|
||
if stray:
|
||
fails.append("图像越界 %d 个(只许 %s): %s" %
|
||
(len(stray), "、".join(IMAGE_PREFIXES), stray[:8]))
|
||
|
||
# 4) 退役路径
|
||
for rel in RETIRED:
|
||
if (ROOT / rel).exists():
|
||
fails.append("退役路径再现: %s/(该目录已消亡,内容按 CLAUDE.md 终态归属;此路径出现即结构回潮)" % rel)
|
||
|
||
# 5) design/ 一级子目录
|
||
design_subs = {p.name for p in (ROOT / "design").iterdir() if p.is_dir()} if (ROOT / "design").is_dir() else set()
|
||
extra_d = design_subs - DESIGN_SUBDIRS
|
||
if extra_d:
|
||
fails.append("design/ 子目录越界: %s(合法: %s;新分类先过用户拍板再改两处白名单)" % (sorted(extra_d), sorted(DESIGN_SUBDIRS)))
|
||
|
||
notes.append("顶层目录 %d 合法 · 根文件 %d 合法 · design/ 子目录 %s · 图像白名单命中 %s" %
|
||
(len(top_dirs & TOP_DIRS), len(top_files & ROOT_FILES),
|
||
sorted(design_subs), "、".join(IMAGE_PREFIXES)))
|
||
return fails, notes
|
||
|
||
|
||
def main():
|
||
fails, notes = scan()
|
||
for n in notes:
|
||
print("✓ " + n)
|
||
if fails:
|
||
print("\n【结构违例】×%d" % len(fails))
|
||
for f in fails:
|
||
print(" ! " + f)
|
||
print("\n处理: 文件归位到合法区;确需新类别 = 同批修订 CLAUDE.md Project Layout + 本脚本白名单,commit 留痕。")
|
||
sys.exit(1)
|
||
print("\n结构审计 PASS(四查全过:顶层目录/根文件/图像白名单/退役路径)")
|
||
sys.exit(0)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|