#!/usr/bin/env python3 # -*- coding: utf-8 -*- """仓库结构审计闸(熵增闸)。 收口第四闸(与 doc_link_audit / doc_term_audit / gdd_lint 同列): 把目录三层裁定变成机器检查——任何会话乱放文件,收口时此处直接 FAIL。 六查: 1. 顶层目录白名单(新增顶层目录 = FAIL,须同批改 AGENTS.md + 本脚本白名单) 2. 根目录文件白名单(根上只许运行文件与规则文件) 3. 图像白名单(图只许在 artwork/ game/assets/ design/art/references/ design/engineering/architecture/ game/tests/golden/) 4. 退役路径守卫(production/ docs/ tests/ qa-data/ design/registry/ design/qa/ 旧路径再现 = FAIL) 5. design/ 一级子目录白名单(防临时新目录) 6. design/art/ 内容白名单(只许 .md 与 references/;审计报告属中间文档归 reports/) 用法: python tools/repo_structure_audit.py # 全查,FAIL 时 exit 1 python tools/repo_structure_audit.py --json # 机器读输出 依据: AGENTS.md「Project Layout」节。 """ import sys from pathlib import Path ROOT = Path(__file__).resolve().parent.parent # 1) 顶层目录白名单(点开头目录一律忽略) # reports/:迭代实现报告/GDD 反哺点 # 清单的家——迭代批禁写 design/,只准往这里写,design/ 全目录对它只读。只放 .md,不放图(图像白名单不含此目录)。 TOP_DIRS = {"archive", "artwork", "design", "game", "sim-data", "tools", "reports"} # 2) 根目录文件白名单 ROOT_FILES = {"AGENTS.md", "README.md", "roadmap.md", "待办清单.md", "体验验证清单.md", "stage.txt", "review-mode.txt", "playnote.md", ".gitignore", ".zcodeignore"} # 3) 图像允许目录(相对路径前缀;archive/ 为归档 zip 不扫) # game/tests/golden/(棋盘 golden 基线=机检测试资产非美术素材,素材齐盘态生成入库【基线换代权限 Q6】) IMAGE_EXTS = {".png", ".jpg", ".jpeg", ".webp", ".gif", ".svg", ".pxo", ".psd", ".bmp"} IMAGE_PREFIXES = ("artwork/", "game/assets/", "design/art/references/", "design/engineering/architecture/", "game/tests/golden/") # 4) 退役路径(目录或文件任一再现即 FAIL) RETIRED = ["production", "docs", "tests", "qa-data", "design/registry", "design/research", "design/art/candidates", "production/mockups", "design/ux", "game/tools", "design/qa", "game/smoke"] # 5) design/ 一级子目录白名单(防 ad-hoc 新目录) DESIGN_SUBDIRS = {"art", "engineering", "gdd", "content-archive"} # 6) design/art/ 内容白名单——美术只放 .md 文档与 references/ 参考图; # 审计报告属中间文档,按 doc-conventions §11 落 reports/,禁驻 art/ ART_EXTS = {".md"} ART_SUBDIRS = {"references"} 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(新增顶层目录须同批改 AGENTS.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/(该目录已消亡,内容按 AGENTS.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))) # 6) design/art/ 内容白名单 art_dir = ROOT / "design" / "art" art_bad = [] if art_dir.is_dir(): for p in sorted(art_dir.iterdir()): rel = p.relative_to(ROOT).as_posix() if p.is_dir(): if p.name not in ART_SUBDIRS: art_bad.append(rel + "/") elif p.suffix.lower() not in ART_EXTS: art_bad.append(rel) if art_bad: fails.append("design/art/ 越界内容 %d 项(只许 .md 与 %s): %s" % (len(art_bad), "、".join(ART_SUBDIRS) + "/", art_bad[:8])) 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处理: 文件归位到合法区;确需新类别 = 同批修订 AGENTS.md Project Layout + 本脚本白名单,commit 留痕。") sys.exit(1) print("\n结构审计 PASS(六查全过:顶层目录/根文件/图像白名单/退役路径/design 子目录/art 内容)") sys.exit(0) if __name__ == "__main__": main()