用户裁定(2026-10-17)五条:①代码批只跑代码闸、文档批只跑文档闸;②迭代批是代码实现批,
禁写 design/,文档只读、是实现批写测试与代码的输入依据;③迭代实现报告不写在 design/ 内;
④不设文档编写 skill——文档由用户在会话中讨论后落地;⑤本批属工具/规则面,不占迭代号。
一、tools/run_all_gates.sh(域划分唯一出处)
- 新增 `--code`(01 冒烟/02 单元/03 探针 --click/04 探针 --full/05 渲染闸/09 静态扫描/11 回放对拍)
与 `--docs`(06 gdd_lint/07 doc_link/08 doc_term/08b active.md 体量);10 熵增闸两域都含
(文档批新增或挪动 .md 同样会绊它,且纯文本秒级);不带参数=全量,改闸件本身或跨两域时用。
- 域外闸不入 NAMES,不产生假变化;同时修「基线绿 → 现 SKIP」被误判为闸况变化(--quick 回执
此前永远自带三条假红)。
- 回执增 domain 与 not_run 字段。
- 导出 PYTHONIOENCODING=utf-8:GBK 控制台下 06/10 两闸打印 ✅/✗ 直接崩(假红)、09 的 FAIL 行
写成乱码字节致指纹漂移(假「红项清单变化」)、末尾汇总自身崩打不出结论——2026-10-16 实踩。
- 新增代码域素材齐盘预检:game/assets/art/** 与一切 *.import 边车都不入 git(2026-09-29 裁定),
worktree 冷检出必空盘 → main_theme.tres 解析失败 → 冒烟 V1 七项假红。缺位即拒跑并列缺哪件,
附处置命令(cp 主区素材 + --editor --quit 刷导入;--import 不生成新素材边车)。
二、tools/render_gate.sh(空跑判绿改判红)
- 棋盘/菜单两段 golden 在素材缺位时原为 SKIP,而闸末照样打印「全绿」exit 0——一个像素都没比
却报通过。2026-10-16 菜单废基线事故正是靠这个信号盲区混过 worktree 自检的。现改判红。
三、reports/ 新顶层目录(实现批唯一可写目录)
- 收迭代实现报告/代拍板追认单/GDD 反哺点清单;repo_structure_audit.py TOP_DIRS 与 CLAUDE.md
Project Layout 两处白名单同批改齐(熵增闸自陈的强制口径);reports/README.md 立范围与生命周期。
四、规则文本五处同步
- CLAUDE.md:验收口径改按域;硬性红线新增「两类批」条(迭代批禁写 design/ 与根规则文件、
产出只落 reports/、只跑代码域闸;非迭代批不占迭代号);Project Layout 登记 reports/。
- process-conventions:§3.10 批末按域验收;§3.12 补 active.md 例外(会话状态唯一源,钩子本就
豁免);§3.13 重写为「实现批禁写文档、产出物落 reports/、反哺点由文档批裁决后写入」;
§4 分级周期口径改三档;§2 三段流程「三·实现」行加禁写面。
- doc-conventions 载体分工表:迭代实现报告落位由 design/qa/ 改 reports/。
- architecture.md「测试与质量域」:新增「改动域划分」节(域表+唯一出处声明+素材预检缘由),
本表是现状登记非第二份清单。
- 06-待办清单:登记本批两处代拍板(active.md 例外读法、reports/ 新顶层目录)与旧实现报告
iter27/iter28 两份仍在 design/qa/ 未迁位的欠账。
五、连带(不在本仓库,gitignored 本地 harness 配置)
- .zcode/hooks/worktree-guard.sh:合回校验由「不碰 game/ 即全免」改为按 diff 路径推定所需域、
要求回执 domain 覆盖之;未登记路径按最严处理,禁默认放行。旧豁免的漏洞:改闸件本身反而零验证。
- ~/.zcode/skills/iter/SKILL.md:顶部加批性质与闸域两条强制声明;批末重闸移出 doc 三闸;
总收口产出物改落 reports/、roadmap 换代移出本批;定义面入库注明是搬运非撰写。
验证:--docs 域全绿(06 由基线红改善为绿,因 fcb8c3f8 已补 01 篇流程图);素材预检反例实测
(移走 button_stone.png → 拒跑 exit 1,还原后正常);渲染闸缺盘反例实测(exit 1 并报 FAIL);
--code --docs 互斥与非法参数 exit 2;guard 内嵌 python 编译通过并按 docs 域回执放行 docs 批。
167 lines
10 KiB
Bash
167 lines
10 KiB
Bash
#!/usr/bin/env bash
|
||
# 渲染闸正主(迭代 19 卡① 自动化验收补齐批 2026-10-08 落码——T1/T2)——窗口化自动闸。
|
||
# 迭代 15③ 挂账「渲染五闸」机器补跑半随本闸落籍(shot_probe/golden_compare/board_shot 三件入闸;
|
||
# fx_probe 逻辑层归 probe_gate --full——归宿对账见卡①任务书 §一;
|
||
# ui_layout_probe 原列此处,2026-10-15 用户裁「无使用的探针删除」随批退役,1920 档布局机检面待补)。
|
||
# 骨架复用 probe_gate:①每段 timeout 守卫(挂死=FAIL 不静默)②段前预清+段后清本 game 目录孤儿
|
||
# Godot(按命令行精确匹配本 game 路径,不误杀其他 worktree 会话)③末尾残留清点=批末闸(必须 0)。
|
||
# 段:
|
||
# ① shot_probe 七屏窗口化截图(menu/settings/career/camp/level/battle/status——迭代 19 卡① T1 扩屏)
|
||
# → tools/shot_checker.py A-D 语义断言自动判定(A 屏级骨架全屏/B 文字对比度/C 明度三档带/
|
||
# D 文字禁压纹理;E 焦点预算=语义面仍归人闸不机检)。首跑存量 FAIL 按 S1 停机点全量呈报登记 06。
|
||
# ② 取证段(Q7:超时/零字节/文件缺失任一即 FAIL——「渲染真实发生」必须成立,不做像素判定):
|
||
# fx_shot_probe 三时刻抓帧(行到达/结算排播/轮末连帧)+ board_shot_probe L1-L3 棋盘抓帧。
|
||
# ③ 棋盘 golden 逐像素(T2):board 整屏帧按 board_meta.json 取样坐标裁剪棋盘区
|
||
# (tools/golden_board_crop.py——禁硬编码像素)→ tools/golden_compare.py 对
|
||
# game/tests/golden/ 三张基线(容差 provisional:差 ≤16/changed ≤2%,S2 停机点口径)。
|
||
# 素材缺位=判红(2026-10-17 改;此前是 SKIP 不判红——但缺盘时这一段根本不对比,闸末仍打
|
||
# 「全绿」exit 0,等于空跑判绿。基线生成期素材缺位=L3 停机,运行期同样无权出验收结论)。
|
||
# ④ 基准档 1280 单档(Q4)。1920 档注记:16:9 全屏下 stretch=canvas_items/aspect=keep 等比
|
||
# 1.35×+左右黑边各 96px,构图与基准档一致(迭代 9 分辨率案甲)——分辨率维度不重复建闸。
|
||
# (原由 ui_layout_probe 两档断言承担,探针已随 2026-10-15 裁「无使用的探针删除」退役,
|
||
# 1920 布局机检面现无承载,待补。)
|
||
# 过程产物纪律(Q2):一切截图 PNG 落 mktemp 临时区,闸毕即弃——任何过程 PNG 禁入 git;
|
||
# 例外=game/tests/golden/ 基线(测试资产入库,.gitignore 已核不挡)。
|
||
# 窗口化运行会短暂弹 Godot 窗口(本机可开窗口径,2026-10-07 用户令);禁改 --headless 跑本闸
|
||
# (headless 无渲染面,探针内 fail-fast 护栏会红——那是护栏生效不是闸红)。
|
||
# 用法: tools/render_gate.sh [game目录] 缺省 game 目录=脚本所在仓库的 game/(worktree 内自动指向该 worktree)
|
||
# 依赖: Git Bash(coreutils timeout/taskkill) + PowerShell + python(PIL);Godot 路径可用环境变量 GODOT_BIN 覆盖。
|
||
set -u
|
||
G="${GODOT_BIN:-/d/code/godot/Godot_v4.7.2-stable_win64_console.exe}"
|
||
REPO="$(cd "$(dirname "$0")/.." && pwd)"
|
||
GAME="${1:-$REPO/game}"
|
||
GAME_WIN="$(cygpath -m "$GAME")" # Godot --path 需 Windows 形式
|
||
TMP="$(mktemp -d /tmp/rendergate_XXXXXX)"
|
||
trap 'rm -rf "$TMP"' EXIT
|
||
_rc=0
|
||
|
||
orphans() { # 列出命令行含本 game 路径的 Godot 进程 PID(console wrapper+真引擎都算)
|
||
printf "Get-CimInstance Win32_Process -Filter \"Name like 'Godot%%'\" | Where-Object { \$_.CommandLine -like '*%s*' } | ForEach-Object { \$_.ProcessId }\n" "$GAME_WIN" > "$TMP/k.ps1"
|
||
powershell -NoProfile -ExecutionPolicy Bypass -File "$(cygpath -w "$TMP/k.ps1")" 2>/dev/null | tr -d '\r'
|
||
}
|
||
kill_orphans() {
|
||
local p; for p in $(orphans); do taskkill //F //PID "$p" >/dev/null 2>&1; done
|
||
}
|
||
|
||
# 冷检出刷导入(.godot 不入 git,fresh worktree 首跑一次性付)
|
||
if [ ! -f "$GAME/.godot/uid_cache.bin" ]; then
|
||
echo "[render_gate] 冷检出:editor --quit 刷导入(一次性,可到 1-2 分钟)"
|
||
timeout 300 "$G" --headless --path "$GAME_WIN" --editor --quit >/dev/null 2>&1
|
||
kill_orphans
|
||
fi
|
||
|
||
pre="$(orphans | wc -l | tr -d ' ')"
|
||
if [ "$pre" -gt 0 ]; then echo "[render_gate] 预清本闸残留 Godot×$pre"; kill_orphans; fi
|
||
|
||
echo "======== shot_probe 七屏截图 → shot_checker A-D ========"
|
||
SHOT="$TMP/shots"
|
||
timeout 180 "$G" --path "$GAME_WIN" res://smoke/shot_probe.tscn -- "$SHOT" >"$TMP/shot.log" 2>&1
|
||
st=$?; kill_orphans
|
||
grep -E "\[shot_probe\]" "$TMP/shot.log" | tail -12
|
||
if [ "$st" -eq 124 ] || [ "$st" -eq 137 ]; then
|
||
echo "[render_gate] FAIL — shot_probe 超时挂死(exit=$st),孤儿已清"; _rc=1
|
||
elif [ "$st" -ne 0 ] || ! grep -q "问题=0" "$TMP/shot.log"; then
|
||
echo "[render_gate] FAIL — shot_probe 未干净完成(exit=$st,缺失屏/空文件/路径断裂见上)"; _rc=1
|
||
fi
|
||
if [ "$_rc" -eq 0 ]; then
|
||
python tools/shot_checker.py "$SHOT" shot_menu.png shot_settings.png shot_career.png shot_camp.png shot_level.png shot_battle.png shot_status.png | sed 's/^/[render_gate] /'
|
||
_rc="${PIPESTATUS[0]}"
|
||
[ "$_rc" -ne 0 ] && echo "[render_gate] FAIL — shot_checker A-D 断言存在红(S1 停机点:首跑存量 FAIL 全量呈报登记 06)"
|
||
fi
|
||
|
||
echo "======== fx_shot_probe 三时刻取证(Q7) ========"
|
||
FX="$TMP/fxshots"
|
||
timeout 180 "$G" --path "$GAME_WIN" res://smoke/fx_shot_probe.tscn -- "$FX" >"$TMP/fx.log" 2>&1
|
||
st=$?; kill_orphans
|
||
grep -E "\[fx_shot_probe\]" "$TMP/fx.log" | tail -4
|
||
if [ "$st" -eq 124 ] || [ "$st" -eq 137 ]; then
|
||
echo "[render_gate] FAIL — fx_shot_probe 超时挂死(exit=$st),孤儿已清"; _rc=1
|
||
elif [ "$st" -ne 0 ]; then
|
||
echo "[render_gate] FAIL — fx_shot_probe 退出码=$st(取证失败=闸红)"; _rc=1
|
||
fi
|
||
fx_evidence() { # 取证双检:模式文件存在且非零字节(零字节/缺失任一即 FAIL——Q7)
|
||
local pat="$1" min="$2" n=0 f
|
||
for f in "$FX"/$pat; do
|
||
[ -f "$f" ] || continue
|
||
[ -s "$f" ] || { echo "[render_gate] FAIL — 取证零字节: $f"; _rc=1; return; }
|
||
n=$((n+1))
|
||
done
|
||
if [ "$n" -lt "$min" ]; then echo "[render_gate] FAIL — 取证文件缺失: $pat 仅 $n/<$min"; _rc=1; fi
|
||
}
|
||
fx_evidence "fx_dig*.png" 1
|
||
fx_evidence "fx_settle*.png" 1
|
||
fx_evidence "fx_round*.png" 6
|
||
fx_evidence "fx_final.png" 1
|
||
|
||
echo "======== board_shot_probe L1-L3 取证(Q7) ========"
|
||
BD="$TMP/boardshots"
|
||
timeout 180 "$G" --path "$GAME_WIN" res://smoke/board_shot_probe.tscn -- "$BD" >"$TMP/bd.log" 2>&1
|
||
st=$?; kill_orphans
|
||
grep -E "\[board_shot\]" "$TMP/bd.log" | tail -6
|
||
if [ "$st" -eq 124 ] || [ "$st" -eq 137 ]; then
|
||
echo "[render_gate] FAIL — board_shot_probe 超时挂死(exit=$st),孤儿已清"; _rc=1
|
||
elif [ "$st" -ne 0 ]; then
|
||
echo "[render_gate] FAIL — board_shot_probe 退出码=$st(取证失败=闸红)"; _rc=1
|
||
fi
|
||
for b in board_L1_dungeon.png board_L2_underdark.png board_L3_deep_underdark.png board_meta.json; do
|
||
if [ ! -s "$BD/$b" ]; then echo "[render_gate] FAIL — 取证缺失/零字节: $b"; _rc=1; fi
|
||
done
|
||
|
||
echo "======== 棋盘 golden 逐像素(T2——裁剪→对比,容差 provisional) ========"
|
||
if [ ! -d "$REPO/game/tests/golden" ]; then
|
||
echo "[render_gate] FAIL — game/tests/golden/ 基线目录缺失(基线在库=验收断言②)"; _rc=1
|
||
elif [ ! -s "$BD/board_meta.json" ]; then
|
||
echo "[render_gate] SKIP — board_meta.json 缺失(取证段已红),golden 对比不判"
|
||
else
|
||
# 素材缺位=判红(2026-10-17 由 SKIP 改):缺盘态下这一段一个像素都不比,而闸末照样打印
|
||
# 「全绿」退出码 0——那是空跑判绿,比误红危险得多(2026-10-16 菜单废基线事故正是靠它蒙过
|
||
# worktree 自检的)。基线锚在素材齐盘态(Q5),缺盘态本就无权出验收结论。
|
||
_asset_miss=0
|
||
for a in 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" ] || { echo "[render_gate] FAIL — 棋盘素材缺位: $a(Q5:基线锚素材齐盘态,缺盘不得出验收结论)"; _asset_miss=1; }
|
||
done
|
||
if [ "$_asset_miss" -eq 0 ]; then
|
||
CROP="$TMP/crop"
|
||
python tools/golden_board_crop.py "$BD/board_meta.json" "$BD" "$CROP" | sed 's/^/[render_gate] /'
|
||
if [ "${PIPESTATUS[0]}" -ne 0 ]; then
|
||
echo "[render_gate] FAIL — 棋盘区裁剪失败"; _rc=1
|
||
else
|
||
python tools/golden_compare.py "$REPO/game/tests/golden" "$CROP" \
|
||
board_L1_dungeon.png board_L2_underdark.png board_L3_deep_underdark.png | sed 's/^/[render_gate] /'
|
||
if [ "${PIPESTATUS[0]}" -ne 0 ]; then
|
||
echo "[render_gate] FAIL — golden 逐像素超容差(S2 停机点:字体/平台误红呈报后定容差)"; _rc=1
|
||
fi
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
echo "======== 菜单 golden 逐像素(迭代 28 T4——启动态全屏基线)========"
|
||
if [ ! -s "$REPO/game/tests/golden/menu_boot.png" ]; then
|
||
echo "[render_gate] FAIL — game/tests/golden/menu_boot.png 基线缺失(菜单 golden 未建——tools/golden_baseline_refresh.sh --menu)"; _rc=1
|
||
elif [ ! -s "$SHOT/shot_menu.png" ]; then
|
||
echo "[render_gate] SKIP — shot_menu.png 缺失(shot_probe 段已红),菜单 golden 不判"
|
||
elif [ ! -f "$GAME/assets/fonts/ui_font_fusion_pixel_zh_hans.ttf" ] || [ ! -f "$GAME/assets/art/ui/button_stone.png" ]; then
|
||
echo "[render_gate] FAIL — 菜单素材缺位(字体/按钮石纹):基线锚素材齐盘态,缺盘不得出验收结论(2026-10-17 由 SKIP 改判红)"; _rc=1
|
||
else
|
||
MENUG="$TMP/menugolden"
|
||
mkdir -p "$MENUG"
|
||
cp "$SHOT/shot_menu.png" "$MENUG/menu_boot.png"
|
||
python tools/golden_compare.py "$REPO/game/tests/golden" "$MENUG" menu_boot.png | sed 's/^/[render_gate] /'
|
||
if [ "${PIPESTATUS[0]}" -ne 0 ]; then
|
||
echo "[render_gate] FAIL — 菜单 golden 超容差(S2 停机点口径:核对热区——按钮换墨/布局变更属预期面=显式重制基线+呈报;超预期面=L3)"; _rc=1
|
||
fi
|
||
fi
|
||
|
||
echo "======== 残留检查(批末闸) ========"
|
||
left="$(orphans | wc -l | tr -d ' ')"
|
||
total="$(tasklist 2>/dev/null | grep -ci '^Godot' || true)"
|
||
echo "本闸残留 Godot=$left(必须0) 全局 Godot 进程=$total(其他会话的仅报数不算账)"
|
||
if [ "$left" -ne 0 ]; then echo "[render_gate] FAIL — 本闸残留未清零"; kill_orphans; _rc=1; fi
|
||
if [ "$_rc" -eq 0 ]; then echo "[render_gate] 全绿"; else echo "[render_gate] 存在 FAIL,见上"; fi
|
||
exit "$_rc"
|