Files
gplan-2/tools/repo_structure_audit.py
T

126 lines
6.0 KiB
Python
Raw 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.
#!/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()