- 40 张取证照与同名对比报告 → game/tests/evidence/ui-audit-2026-10-06/(该目录既有章程「人工测试证据」,目录根新增 .gdignore 令 Godot 整目录跳过导入,图不生成 .import 边车也不进导出包;parity/anchor.json 走文件系统路径读取不受影响) - 有接收方的两份审计(代码术语审计→06:546+迭代31 卡①/测试审计→测试分层卡片)平移 design/research/;iter27-探针迁移对照(T1 未施工=活交付物)迁 reports/,reports/README 放什么表补一行交付物类型 - 已核销的迭代18-实机目检总清单(2026-10-14 十八项全过)删除,顺带清掉其内 4 条指向8e02dcf0已删件的回指 - doc-conventions §11:暂存地唯一归 design/research/,验证凭据不归文档目录;新增「落档必写接收方」一行——写不出接收方的不落文件,内容在会话里交付 - tools/balance_verifier.py 报告与原始数据改落 sim-data/(QA_DIR→READINGS_DIR) - 活指针 13 处改判:AGENTS.md:22/30 与 Project Layout game 行、06:546、07:103、career 卡片×4、art-bible §8.2 L490 与 生涯.md:81 两处8e02dcf0遗留死指涉、game/tests/README 分级表与台账行、reports/README - 结构闸白名单三处同批收口:IMAGE_PREFIXES 去 design/qa/ 加 game/tests/evidence/、DESIGN_SUBDIRS 去 qa、RETIRED 加 design/qa(目录复活即 FAIL) - .qoder 技能 doc-consistency-check 两处指路改判(harness 配置不入 git,主区直接改) 单件试跑全绿:gdd_lint 10/10、doc_link --strict 死链 0、doc_term --strict 疑似现行 0、pipeline_gate 63 条目/21 卡、repo_structure_audit PASS。 本批 diff 触及 game/ 路径,合回需全量档回执(含代码域测试),待用户点名。
106 lines
5.3 KiB
Python
106 lines
5.3 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""仓库结构审计闸(熵增闸,2026-09-27 用户立项)。
|
||
|
||
收口第四闸(与 doc_link_audit / doc_term_audit / gdd_lint 同列):
|
||
把目录三层裁定变成机器检查——任何会话乱放文件,收口时此处直接 FAIL。
|
||
|
||
四查:
|
||
1. 顶层目录白名单(新增顶层目录 = FAIL,须同批改 AGENTS.md + 本脚本白名单)
|
||
2. 根目录文件白名单(根上只许运行文件与规则文件)
|
||
3. 图像白名单(图只许在 artwork/ game/assets/ design/art/references/ design/dev/architecture/)
|
||
4. 退役路径守卫(production/ docs/ tests/ qa-data/ candidates/mockups 旧路径再现 = FAIL)
|
||
|
||
用法: python tools/repo_structure_audit.py # 全查,FAIL 时 exit 1
|
||
python tools/repo_structure_audit.py --json # 机器读输出
|
||
依据: AGENTS.md「Project Layout」节(2026-09-27 目录三层收敛终态)。
|
||
"""
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
ROOT = Path(__file__).resolve().parent.parent
|
||
|
||
# 1) 顶层目录白名单(点开头目录一律忽略)
|
||
# reports/ 收编(2026-10-17 用户裁「迭代批禁写 design/」):迭代实现报告/代拍板追认单/GDD 反哺点
|
||
# 清单的家——实现批只准往这里写,design/ 全目录对它只读。只放 .md,不放图(图像白名单不含此目录)。
|
||
TOP_DIRS = {"archive", "artwork", "design", "game", "sim-data", "tools", "reports"}
|
||
# 2) 根目录文件白名单
|
||
ROOT_FILES = {"AGENTS.md", "README.md", "roadmap.md", "stage.txt", "review-mode.txt",
|
||
".gitignore", ".zcodeignore"}
|
||
# 3) 图像允许目录(相对路径前缀;archive/ 为归档 zip 不扫)
|
||
# game/tests/golden/ 收编(迭代 19 卡① T2/T6 2026-10-08——棋盘 golden 基线=机检测试资产非美术素材,
|
||
# 素材齐盘态生成入库【基线换代权限 Q6】;T1 落库时漏改本白名单致熵增闸红,同批修订 commit 留痕)
|
||
# game/tests/evidence/ = 验证取证照与它的对比报告(目录根 .gdignore 令 Godot 跳过导入,图不进包)
|
||
IMAGE_EXTS = {".png", ".jpg", ".jpeg", ".webp", ".gif", ".svg", ".pxo", ".psd", ".bmp"}
|
||
IMAGE_PREFIXES = ("artwork/", "game/assets/", "design/art/references/", "design/dev/architecture/",
|
||
"game/tests/golden/", "game/tests/evidence/")
|
||
# 4) 退役路径(目录或文件任一再现即 FAIL)
|
||
RETIRED = ["production", "docs", "tests", "qa-data", "design/registry",
|
||
"design/art/candidates", "production/mockups",
|
||
"design/ux", "game/tools", "design/qa"]
|
||
# design/ 一级子目录白名单(防 ad-hoc 新目录)
|
||
DESIGN_SUBDIRS = {"art", "dev", "gdd", "research"}
|
||
|
||
|
||
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)))
|
||
|
||
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(四查全过:顶层目录/根文件/图像白名单/退役路径)")
|
||
sys.exit(0)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|