126 lines
6.0 KiB
Python
126 lines
6.0 KiB
Python
#!/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",
|
||
"game/tools", "design/qa", "game/smoke"]
|
||
# 5) design/ 一级子目录白名单(防 ad-hoc 新目录)
|
||
DESIGN_SUBDIRS = {"art", "engineering", "gdd", "ux", "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()
|