Files
gplan-2/tools/run_all_gates.sh
T
DKChan 27812167ef refactor(状态规矩): 废除根目录会话状态文件 active.md 与全套执法件(2026-10-18 用户裁)
判据(对 869 个 ZCode 会话与 16 个 Qoder 会话的读写统计):active.md 被整文件读 448 次、
单次中位数 14 KB、累计 6.09 MB 进上下文,311/320 个碰它的会话把它当第一个工具调用读;
但 7311 个提及它的思考片段的语境分布是——写入与编号簿记 327 个会话、并行冲突与避让 234 个、
退役与失真核对 162 个,而明确写「从它所载内容得知某事实」的只有 20 个会话。
逐条核 41 个条目:独特且现行 2 条(另 3 个子句)、别处已有权威源 14 条、陈旧或自相矛盾 7 条、
已完结批次的历史叙述 18 条;按字节约 95% 属后三类。失实样本:architecture 版本记 v1.6 实为 v1.8、
规则文件数记 7 份实为 6 份、迭代 27/28 记「位预留追加卡」而 roadmap 记已完成、三个已删 worktree
仍记「在盘待删」、同一条既宣称「文档禁列闸清单」又自行列出 01–11 全套闸。
执法件已建一轮仍拦不住:08b 体量闸只测条目数/行数/编号/死链,测不出语义失真;钩子不随仓库传播,
只有 ZCode 在跑;10-15 清到 8.6 KB、10-17 回到 40 KB,用户为「只写不删」连续三次发火。

改动面:
- 删 active.md 与 tools/audit_active_state.py;run_all_gates.sh 摘除 08b 闸件与头注、
  baseline.json 删 08b 记录(现存 12 闸)、repo_structure_audit.py 根文件白名单去 active.md
- doc-conventions §10 改写为「仓库根不设状态文件」并留裁决与判据(保持 §11/§12 编号不漂);
  §11 表两行去状态文件口径;§4 适用范围去 active.md
- process-conventions §1 第 2/3 条(合回前查尾号防撞号、豁免面)、§3 第 12 条(批次完成后的
  登记义务)、§6 分级周期口径(闸件清单不再在文档重列)四处换代
- code-conventions「实现批收尾双登记」改为单登记;CLAUDE.md 目录节、规范导航行、worktree 豁免
  行三处换代,红线新增「仓库根禁止设会话状态文件」一条
- roadmap.md 顶部新增「当前位」三行(阶段/迭代位/下一步)接替原快照职责,头注、执行纪律第 3 条、
  loop 规范第 8 条、维护纪律四处改指 06 与本文;reports/README.md 与 sim-data/README.md 两处
  现行指路改口;architecture.md 文档域闸表行去 active.md 体量闸与触发路径
- 06-待办清单迁入七条真残值(原仅载于该文件):卡片入库纪律悬空待办、req-card 两副本取齐、
  魔法道具与罕见残留(实数 23/17 处,旧记约 30/18)、通关口径统一(gdd 内 188 处,旧记 273)、
  黑话第二波待发令、数据整改批九条存疑待抽查(该批未提交面所载)、发布前删除 main.gd:196 调试
  入口写死行(工程直入·待写卡)

harness 侧另批处理(.zcode/ 与用户级技能不入 git):session-snapshot.sh 与 config.json 注册、
worktree-guard.sh 的 active.md 豁免白名单、active-closing 技能、doc-consistency-check 第 8/9 项、
iter 与 req-card 技能里的「唯一例外=active.md」与查尾号条款。
2026-10-18 07:28:48 +08:00

373 lines
18 KiB
Bash
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 bash
# 重闸唯一入口(2026-10-15 用户令「迭代完成前必须全量跑一遍所有测试」)——
# 闸清单的唯一定义处。文档只写「收口跑本脚本」,禁在文档里另列闸件清单(那正是 iter27
# 漏跑 probe_gate --full 的成因:清单散落三份文档、彼此不一致、且无机器校验)。
#
# 跑完写回执 game/tests/.gates/receipt.json(commit + 各闸退出码 + 存量基线比对 + 时间戳);
# .zcode/hooks/worktree-guard.sh 在主工作区 git merge 时校验本回执,缺或红即拦截。
#
# 用法:tools/run_all_gates.sh [--code|--docs|--quick]
# --code 只跑代码域闸(01 冒烟/02 单元/03 探针 --click/04 探针 --full/05 渲染闸/09 静态扫描
# /11 回放确定性对拍+10 熵增闸)——本批改动触及 game/ 时用(游戏工程的唯一代码验收域)。
# --docs 只跑文档域闸(06 gdd_lint/07 doc_link/08 doc_term/08c 流水线闸
# +10 熵增闸)
# ——本批改动不触及 game/ 时用:design/、根规则文件、tools/ 工具脚本、.zcode/ 配置等
# 一切非 game 改动的本域验收。
# --quick 全量档的中段自检形态:跳过渲染闸(窗口化,需开显示器)与静态扫描;完成出库禁用。
# 不带参数 = 全量档(两域全跑),触发条件=同批混触 game/ 与文档面。闸件(tools/ 下机检脚本
# 与闸壳)自身的改动不占合回全量——其质量义务=落批时的变异验证(test-conventions §6);
# 闸真崩溃时合回闸 fail-closed(跑不出回执即拦),不以全量为兜底。
# **域划分只在本脚本里定义一处**——文档只写「按改动域跑本脚本」,禁另列闸清单(清单散落多处
# 正是 iter27 漏跑 probe_gate --full 的成因)。
#
# 判定口径:每道闸与 game/tests/.gates/baseline.json 里的存量基线比对——
# 基线绿 + 现绿 → 绿
# 基线红 + 现红且清单未变 → 「存量红·对基线零新增」,不判失败(但会在回执里留名)
# 基线红 + 现红但清单新增 → 红(新增红项)
# 任意闸 exit 与基线不符 → 红
# 本次未跑的闸(--quick 跳过/条件闸件未触面)→ 不比闸况,只记进回执 not_run
# 基线文件首次自动生成(把当次结果当基线),此后每次跑批与之比。
# 分域跑批只比本域跑过的闸——域外闸不入 NAMES,不产生「基线绿 → 现红」的假变化。
#
# 退出码:0=全绿或仅存量红零新增;1=有新增红/闸况变化;2=用法错
set -u
REPO="$(cd "$(dirname "$0")/.." && pwd)"
GAME="$REPO/game"
STAMP_DIR="$GAME/tests/.gates"
RECEIPT="$STAMP_DIR/receipt.json"
BASELINE="$STAMP_DIR/baseline.json"
QUICK=0
DOMAIN="full"
for _a in "$@"; do
case "$_a" in
--quick) QUICK=1 ;;
--code|--docs)
[ "$DOMAIN" = "full" ] || { echo "用法: --code 与 --docs 互斥"; exit 2; }
DOMAIN="${_a#--}" ;;
*) echo "用法: tools/run_all_gates.sh [--code|--docs|--quick]"; exit 2 ;;
esac
done
# 控制台编码:本仓库闸件输出中文与 ✅/✗ 符号。Windows GBK 控制台下 python 打印这些字符会抛
# UnicodeEncodeError——06/10 两闸直接崩(假红)、09 的 FAIL 行被写成乱码字节(指纹漂移,
# 与基线比必判「红项清单变化」假红)、末尾汇总自身崩掉打不出结论。2026-10-16 实踩。
export PYTHONIOENCODING=utf-8
# 回执与日志是中间产物(不入库),但 .gitignore 本身必须入库——漏了就等于把它们纳入版本管理。
# 每次跑批前自愈重建,避免「清目录重跑」把 .gitignore 一起带走(2026-10-15 实踩)。
mkdir -p "$STAMP_DIR"
if [ ! -f "$STAMP_DIR/.gitignore" ]; then
printf '# 本目录只入库 baseline.json(存量基线,需随代码走以保证各 worktree 判定口径一致)。\n# receipt.json 是每跑批覆写的当次回执、*.log 是跑批当场日志——两者都是中间产物,不入库。\nreceipt.json\n*.log\n' \
> "$STAMP_DIR/.gitignore"
echo "[gates] 已自愈重建 $STAMP_DIR/.gitignore"
fi
# ── python 解释器解析:优先带 PIL 的 gamedev venv(渲染闸的 shot_checker/golden_compare 依赖),
# 回落 PATH 上的 python。渲染闸的三个脚本要 PIL,用错解释器会假红。
PY=""
for cand in "/d/dev/python/envs/gamedev/Scripts/python.exe" "$(command -v python 2>/dev/null)" "python"; do
[ -z "$cand" ] && continue
if "$cand" -c "import PIL" >/dev/null 2>&1; then PY="$cand"; break; fi
[ -z "$PY" ] && PY="$cand"
done
GODOT="${GODOT_BIN:-/d/code/godot/Godot_v4.7.2-stable_win64_console.exe}"
# 把带 PIL 的解释器目录前置到 PATH——render_gate.sh / shot_checker.py / golden_compare.py /
# golden_board_crop.py 内部各自调 `python`,继承本 PATH;用错解释器会因缺 PIL 假红。
case "$PY" in
*/Scripts/python.exe|*/Scripts/python) export PATH="$(dirname "$PY"):$PATH" ;;
esac
# ── 域判定:这道闸属哪个域、本次跑不跑 ──
in_domain() { # in_domain <code|docs|both>
local dom="$1"
[ "$dom" = "both" ] && return 0
[ "$DOMAIN" = "full" ] && return 0
[ "$DOMAIN" = "$dom" ] && return 0
return 1
}
# ── 素材齐盘预检(2026-10-17 加)──
# game/assets/art/** 与一切 *.import 边车都不入 git(.gitignore 2026-09-29 裁定「素材不入 git」),
# 所以 git worktree 检出的工程天生缺全部美术素材:main_theme.tres 解析失败 → 按钮五个样式盒全空
# → 冒烟 V1 成片假红;渲染闸又会因素材缺位把两段 golden 直接跳过(见 render_gate.sh)。
# 缺位即拒跑——禁带着空盘跑完,把「什么都没比」的回执当全绿合回。
if in_domain code; then
_miss=""
for a in assets/art/ui/button_stone.png assets/fonts/ui_font_fusion_pixel_zh_hans.ttf \
assets/art/board/flag_seal.png assets/art/character/character_hero_idle.png \
assets/art/board/terrain_wall_n_dungeon.png assets/art/board/terrain_floor_dungeon_1.png \
assets/art/board/terrain_wall_n_underdark.png assets/art/board/terrain_floor_underdark_1.png \
assets/art/board/terrain_wall_n_deep_underdark.png assets/art/board/terrain_floor_deep_underdark_1.png; do
[ -f "$GAME/$a" ] || _miss="$_miss $a"
[ -f "$GAME/$a.import" ] || _miss="$_miss $a.import"
done
[ -f "$GAME/.godot/uid_cache.bin" ] || _miss="$_miss .godot/uid_cache.bin(导入未刷)"
if [ -n "$_miss" ]; then
echo "[gates] 素材齐盘预检 FAIL — 本工作区缺:$_miss"
echo " 处置:cp -r <主工作区>/game/assets/art/. <本区>/game/assets/art/"
echo " 再刷导入:$GODOT --headless --path <本区>/game --editor --quit"
echo " (--import 不生成新素材的 .import 边车,必须走 --editor --quit)"
exit 1
fi
echo "[gates] 素材齐盘预检 PASS(10 件素材+边车+导入缓存在位)"
fi
declare -a NAMES=() EXITS=() LOGS=()
FAILED=0
echo "重闸跑批 · 域=$DOMAIN · 仓库 $REPO"
echo "起跑 commit: $(git -C "$REPO" rev-parse --short HEAD 2>/dev/null || echo '非git目录')"
echo "python: $PY"
echo "godot: $GODOT"
# ── 闸件执行:每道闸的输出落独立日志,供基线比对取指纹 ──
gate() { # gate <名称> <日志名> <命令...>
local name="$1" log="$2"; shift 2
echo ""
echo "════════════════════════════════════════════"
echo " $name"
echo "════════════════════════════════════════════"
"$@" >"$STAMP_DIR/$log" 2>&1
local rc=$?
# 失败时把关键行回显到终端(红必须看得见)
if [ $rc -ne 0 ]; then
grep -E "FAIL|红|Error|error" "$STAMP_DIR/$log" | tail -12 | sed 's/^/ | /'
fi
NAMES+=("$name"); EXITS+=("$rc"); LOGS+=("$log")
return 0
}
gate_unit() { # run_all 是 --script 模式,退出码为唯一判据
local name="02 单元套件"
echo ""
echo "════════════════════════════════════════════"
echo " $name(--script 模式,退出码为唯一判据)"
echo "════════════════════════════════════════════"
( cd "$REPO" && "$GODOT" --headless --path "$(cygpath -m "$GAME")" --script res://tests/run_all.gd ) \
>"$STAMP_DIR/unit.log" 2>&1
local rc=$?
# 三硬闸之二(脚本错误扫描):子套件的 SCRIPT ERROR/Parse Error 只写 stderr,入口内收不到,
# 只能在编排层扫本闸日志。命中即红——此类错误不改退出码、且会中断所在函数的剩余断言。
if [ "$rc" -eq 0 ] && ! "$PY" "$REPO/tools/error_scan.py" "$STAMP_DIR/unit.log" >>"$STAMP_DIR/unit.log" 2>&1; then
rc=1
tail -12 "$STAMP_DIR/unit.log" | sed 's/^/ | /'
fi
[ $rc -ne 0 ] && tail -12 "$STAMP_DIR/unit.log" | sed 's/^/ | /'
NAMES+=("$name"); EXITS+=("$rc"); LOGS+=("unit.log")
return 0
}
# ══ 代码域(1–5)══
if in_domain code; then
# ── 1. 系统级断言面(冒烟三闸:退出码+摘要失败数+SCRIPT ERROR 扫描,判定无洞)──
gate "01 冒烟全量" "smoke.log" "$PY" "$REPO/tools/run_smoke.py"
# ── 2. 单元套件 ──
gate_unit
# ── 3. 场景探针 · 行为走查段 ──
gate "03 探针 --click" "probe_click.log" bash "$REPO/tools/probe_gate.sh" "$GAME" --click
# ── 4. 场景探针 · 断言段(iter27 漏跑的就是这一段)──
gate "04 探针 --full" "probe_full.log" bash "$REPO/tools/probe_gate.sh" "$GAME" --full
# ── 5. 渲染面(窗口化,依赖 PIL)──
if [ $QUICK -eq 0 ]; then
gate "05 渲染闸" "render.log" bash "$REPO/tools/render_gate.sh" "$GAME"
else
NAMES+=("05 渲染闸"); EXITS+=("SKIP"); LOGS+=("")
echo ""; echo "[gates] 05 渲染闸 → SKIP(--quick)"
fi
fi
# ══ 文档域(6–8c)══
if in_domain docs; then
# ── 6. 文档三闸 ──
gate "06 gdd_lint" "gdd_lint.log" "$PY" "$REPO/tools/gdd_lint.py"
gate "07 doc_link" "doc_link.log" "$PY" "$REPO/tools/doc_link_audit.py" --strict
gate "08 doc_term" "doc_term.log" "$PY" "$REPO/tools/doc_term_audit.py" --strict
# ── 8c. 流水线闸(06 条目状态机+roadmap 迭代总表+一次交叉核,判据=doc-conventions §12)──
gate "08c 流水线闸" "pipeline.log" "$PY" "$REPO/tools/pipeline_gate.py"
fi
# ── 6c. 回放确定性对拍(条件闸件——迭代 29 T3 收编;属代码域)──
if in_domain code; then
# 触面判定:本批 diff 相对 master 分叉点,命中下列路径面之一即「触面」。
# 触面也**不自动跑**——对拍调的是 MC 脚本,属「MC/统计类跑批发起前须用户批准」红线管辖;
# 闸只负责每次跑批都把触面判定做一遍并显式要求批准,杜绝「该跑却没人记得」。
# 用户批准后设 PARITY_APPROVED=1 重跑本脚本即实跑三变体对拍。
PARITY_TOUCH_FACES="
game/data/tables/
game/core/rng_service.gd
game/core/save_manager.gd
game/core/meta_progression.gd
game/core/settlement/
game/core/damage_math.gd
game/core/damage_controller.gd
game/core/battle_orchestrator.gd
game/smoke/adventure_monte_carlo.gd
"
_base="$(git -C "$REPO" merge-base master HEAD 2>/dev/null || true)"
_changed_files="$(git -C "$REPO" diff --name-only "${_base:-HEAD}" HEAD 2>/dev/null || true)"
_touch_hit=0
for _f in $_changed_files; do
for _pat in $PARITY_TOUCH_FACES; do
case "$_f" in
"$_pat"*) _touch_hit=1 ;;
esac
done
done
_parity_name="11 回放确定性对拍"
if [ "$_touch_hit" -eq 0 ]; then
NAMES+=("$_parity_name"); EXITS+=("SKIP"); LOGS+=("")
echo ""; echo "[gates] $_parity_name → SKIP(本批 diff 未触数值/存档/RNG/结算面)"
elif [ "${PARITY_APPROVED:-0}" = "1" ]; then
gate "$_parity_name" "parity.log" "$PY" "$REPO/tools/parity_anchor.py" check
else
NAMES+=("$_parity_name"); EXITS+=("NEED-APPROVAL"); LOGS+=("")
echo ""
echo "[gates] $_parity_name → 本批 diff 触及数值/存档/RNG/结算面,对拍需用户批准发起"
echo " (批准后设 PARITY_APPROVED=1 重跑本脚本即实跑;不得借机重锚)"
fi
fi
# ── 7. 静态扫描(代码域)+ 熵增闸(两域都跑:它查顶层目录/根文件/图像白名单/退役路径,
# 文档批新增或挪动 design/ 下的文件同样会绊它;纯文本扫描,秒级)──
if [ $QUICK -eq 0 ]; then
if in_domain code; then
gate "09 静态扫描" "static_scan.log" bash "$GAME/tests/static_scan.sh"
fi
gate "10 熵增闸" "entropy.log" "$PY" "$REPO/tools/repo_structure_audit.py"
else
if in_domain code; then
NAMES+=("09 静态扫描"); EXITS+=("SKIP"); LOGS+=("")
fi
NAMES+=("10 熵增闸"); EXITS+=("SKIP"); LOGS+=("")
echo ""; echo "[gates] 09 静态扫描(代码域)/ 10 熵增闸 → SKIP(--quick)"
fi
# ── 与存量基线比对 ──
"$PY" - "$BASELINE" "$RECEIPT" "$QUICK" "$DOMAIN" "$REPO" "${NAMES[@]}" -- "${EXITS[@]}" -- "${LOGS[@]}" <<'PYEOF'
import hashlib, json, os, subprocess, sys
baseline_path, receipt_path, quick, domain, repo = sys.argv[1:6]
rest = sys.argv[6:]
sep = rest.index("--")
names = rest[:sep]
tail = rest[sep + 1:]
sep2 = tail.index("--")
exits = tail[:sep2]
logs = tail[sep2 + 1:]
stamp_dir = os.path.dirname(receipt_path)
def head():
try:
return subprocess.run(["git", "-C", repo, "rev-parse", "HEAD"],
capture_output=True, text=True, timeout=5).stdout.strip()
except Exception:
return "unknown"
def fingerprint(exit_code, log):
"""红项指纹:只取 exit!=0 时的 FAIL 关键行集合,忽略时序/耗时噪声。
绿闸一律记 'green'——绿闸日志里的「零红」「全绿」等字样不是红项,采进来只会造噪声。"""
# SKIP / NEED-APPROVAL 都属「本闸未实跑」状态,指纹一律记 skip——
# 两者互变不是闸况变化(同是没跑),只有「未跑 → 跑出结果」或「绿 → 红」才算变化。
if exit_code in ("0", "SKIP", "NEED-APPROVAL"):
return "green" if exit_code == "0" else "skip"
if not log:
return "red-nolog"
p = os.path.join(stamp_dir, log)
if not os.path.exists(p):
return "red-nolog"
keys = []
with open(p, encoding="utf-8", errors="replace") as f:
for line in f:
s = line.strip()
if not s or "FAIL" not in s:
continue
keys.append(s[:160])
if not keys:
return "red-nokeys"
return hashlib.sha1("\n".join(sorted(set(keys))).encode("utf-8")).hexdigest()[:12]
now = {}
for n, e, l in zip(names, exits, logs):
now[n] = {"exit": e, "fp": fingerprint(e, l)}
first_run = not os.path.exists(baseline_path)
if first_run:
with open(baseline_path, "w", encoding="utf-8") as f:
json.dump({"note": "存量基线·首次自动生成;闸况变化时本脚本会判红,需人工确认后重生成",
"gates": now}, f, ensure_ascii=False, indent=1)
base = dict(now)
else:
with open(baseline_path, encoding="utf-8") as f:
base = json.load(f).get("gates", {})
changed, inherited_red, not_run = [], [], []
for n in names:
b, c = base.get(n), now[n]
if b is None:
changed.append("%s:无基线记录(新增闸件)" % n)
continue
if b["exit"] == c["exit"] and b["fp"] == c["fp"]:
continue
if c["fp"] == "skip":
# 本闸这次没跑(--quick 跳过/条件闸件未触面)——没有结果可比,不判闸况变化。
# 2026-10-17 修:此前 SKIP 落进「基线绿 → 现红」分支,任何 --quick 回执都自带假红。
not_run.append(n)
continue
if b["exit"] != "0" and c["exit"] != "0" and b["fp"] == c["fp"]:
continue # 同款存量红,清单未变
if b["exit"] == "0" and c["exit"] != "0":
changed.append("%s:基线绿 → 现红(exit=%s)" % (n, c["exit"]))
elif b["exit"] != "0" and c["exit"] == "0":
inherited_red.append("%s:基线红 → 现绿(改善)" % n)
else:
changed.append("%s:红项清单变化(基线 %s → 现 %s)" % (n, b["fp"], c["fp"]))
stock_red = [n for n, e in zip(names, exits) if e not in ("0", "SKIP")]
all_green = not changed
with open(receipt_path, "w", encoding="utf-8") as f:
json.dump({
"commit": head(),
"commit_short": head()[:8],
"timestamp": subprocess.run(["date", "-Iseconds"], capture_output=True, text=True).stdout.strip(),
"quick_mode": quick == "1",
"domain": domain,
"first_baseline_run": first_run,
"gates": [{"name": n, "exit": e, "fingerprint": now[n]["fp"]} for n, e in zip(names, exits)],
"changed_vs_baseline": changed,
"inherited_red": inherited_red,
"not_run": not_run,
"all_green": all_green,
}, f, ensure_ascii=False, indent=1)
print("")
print("─" * 60)
if first_run:
print("首次跑批:已生成存量基线 %s" % baseline_path)
print("本次结果作为基线,后续与之比对。")
if changed:
print("【闸况变化,需处理】")
for c in changed:
print(" ✗ " + c)
if inherited_red:
print("【改善】")
for c in inherited_red:
print(" ↓ " + c)
if not_run:
print("【本次未跑(域外/--quick/未触面)】" + "、".join(not_run))
stock = [n for n, e in zip(names, exits) if e not in ("0", "SKIP")]
if stock:
print("【存量红·对基线零新增】" + "、".join(stock))
if all_green:
print("【验收通过】闸况与基线一致,无新增红。")
sys.exit(0)
sys.exit(1)
PYEOF
RC=$?
echo ""
echo "════════════════════════════════════════════"
printf ' 回执:%s\n 基线:%s\n commit:%s\n' "$RECEIPT" "$BASELINE" "$(git -C "$REPO" rev-parse --short HEAD 2>/dev/null || echo '?')"
echo "════════════════════════════════════════════"
exit $RC