Files
gplan-2/tools/repo_structure_audit.py
T
DKChan 224a947559 merge: 分支并入 master(推进 84 提交,2026-10-21)——research/ 治理批按现master重取四处冲突
解冲突取法=master 现文为准、本批新增语义打上去,不照抄分支旧文本:
- doc-conventions §11:活文档清单与「暂存地非档案库」行取本批新增全文(content-archive 退役内容留底/research/ 无常驻例外/跨会话交接文件禁入库/根 playnote 定性),清单称呼按 docmove 批现文写作「待办清单」,「落档必写接收方」「迭代实现报告」两行照收 master 版(旧日期注记不复活)。
- 08-Alpha版本标准:Alpha 出口裁决与营地五卡入 Alpha 的全部现行行照收 master,仅把 playnote 链接按新位置改指 ../../playnote.md。
- tools/repo_structure_audit.py:ROOT_FILES 取 master 全集(含待办清单/体验验证清单两件根文件)后追加 playnote.md。
- design/research/Alpha未完成事项梳理-2026-10-13.md:modify/delete 冲突按删除解——master 侧自分叉只改过两处指路句(f100e192/281d471e),其结论已由 08 v1.11-v1.12 与待办清单承载,吸收判据今日重核成立。

随批补两处 master 前进后暴露的同步面:AGENTS.md Project Layout 根目录段补 playnote.md 一行(白名单收了新根文件而目录说明未跟上);技能层 batch-closure 两份副本(.zcode 原件→整文件覆盖 .qoder)补「research/ 无常驻例外」的候选呈报口径与 playnote 不入退役候选。
2026-10-21 07:18:31 +08:00

126 lines
6.1 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/;审计报告属中间文档归 design/research/)
用法: 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/art/candidates", "production/mockups",
"design/ux", "game/tools", "design/qa"]
# 5) design/ 一级子目录白名单(防 ad-hoc 新目录)
DESIGN_SUBDIRS = {"art", "engineering", "gdd", "research", "content-archive"}
# 6) design/art/ 内容白名单——美术只放 .md 文档与 references/ 参考图;
# 审计报告属中间文档,按 doc-conventions §11 落 design/research/,禁驻 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()