判据(对 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」与查尾号条款。
373 lines
18 KiB
Bash
373 lines
18 KiB
Bash
#!/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
|