八件受版文件 git mv 进 reports/(一律先迁不删,删除归后续批或用户点名):提案三件(冒险前资源配比预设/效果引擎无状态化/武器锻造系统)、审计两件(代码术语审计/单测与验收门禁标准测试审计)、美术审计两件(cursor 会话存档 md + canvas 转 HTML 渲染件)、需求澄清-营地交互重做;未入库的 UI预览demo 三路对照件只搬盘不入库。 GDD 落文:锻造工坊.md「满级特效扩展位」节收下七条候选池(长剑破甲/长剑暴击面扩大/长枪先手连击/法杖开局魔法矢/轻甲灵巧/重甲反震/魔法护甲护盾),按现役 EFFECT_KINDS 与 settlement.tres:51/52 逐条复核实现代价分档,效果类型闭集口径取正文现行 31 种、不抄过程稿旧记 24 种;候选池定性为备用清单非承诺清单。 引用面换代 08(playnote 之外两处 ../research/ 改 ../../reports/)、待办清单四处、reports 八张卡片(跨目录前缀改同目录)、art-assets-checklist 与 sim-data/README 两处既存死指改「沿革查 git 提交史」、content-archive 出处句标注该目录已退役。迁入件内部相对链接随迁移重基准(../gdd/ → ../design/gdd/,playnote 与待办清单改回仓库根),并订正上一批遗留错误:两张迭代 30 卡片的 playnote 链接原写 ../../playnote.md(从 reports/ 出发指到仓库外),现改 ../playnote.md。 规则与机检:doc-conventions §11「暂存地非档案库」行改为单暂存地+禁在 design/ 下设过程件目录+图像载体与报告渲染件口径分列(禁图留 artwork/ 沿用 2026-09-27 用户裁;.html/.json 允许作为报告的一种=2026-10-21 用户裁,原「只放 .md」句系 10-17 建目录时顺带照搬、无单独点头记录);AGENTS Project Layout 两行同步;reports/README 补「设计讨论段过程稿」命名行。repo_structure_audit:DESIGN_SUBDIRS 去 research、RETIRED 加 design/research;doc_ref_audit 排除面清空(design 全树入扫描面);doc_term/doc_link 扫描域注释换代;三件样张脚本 OUT_REPORT 改落 reports/ 并补 os.makedirs(改前只对 OUT_PNG 建目录,目录缺失即 FileNotFoundError)。 变异验证:重建 design/research 并放文件 → 结构闸退出码 1、同时报「退役路径再现」与「子目录越界」两条;design/ 内放两条指向不存在文件的引用 → 引用可核机检报「路径不存在」红项 2(该闸暂未注册进闸壳,无基线记录必判变化);清场后结构闸 PASS。
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",
|
||
"design/ux", "game/tools", "design/qa"]
|
||
# 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()
|