From a7c63850b726e95009ed241c52659df0c92770c5 Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Sat, 31 Oct 2026 04:28:03 +0800 Subject: [PATCH] =?UTF-8?q?=E5=B7=A5=E7=A8=8B(=E6=8F=90=E4=BA=A4=E7=82=B9?= =?UTF-8?q?=E6=96=87=E6=A1=A3=E5=9F=9F=E5=A2=9E=E9=87=8F=E5=8C=96=E6=89=B9?= =?UTF-8?q?=C2=B7pre-commit=20=E8=A7=A6=E5=8F=91=E7=B2=92=E5=BA=A6):=20run?= =?UTF-8?q?=5Fall=5Fgates.sh=20=E5=A2=9E=E9=87=8F=E6=A1=A3=E2=80=94?= =?UTF-8?q?=E2=80=94--staged=20<=E6=B8=85=E5=8D=95=E6=96=87=E4=BB=B6>=20?= =?UTF-8?q?=E4=BB=85=E9=85=8D=20--docs,=E8=A7=A6=E5=8F=91=E7=9F=A9?= =?UTF-8?q?=E9=98=B5=E5=94=AF=E4=B8=80=E8=90=BD=E7=82=B9(=E4=BB=BB?= =?UTF-8?q?=E4=B8=80=20.md=E2=86=9206/06b/07/08/08d=20=E5=85=88=E6=8C=89?= =?UTF-8?q?=E6=B8=85=E5=8D=95=E5=86=85=20.md=20=E6=89=AB=E6=8F=8F=C2=B7?= =?UTF-8?q?=E5=BE=85=E5=8A=9E=E6=B8=85=E5=8D=95/roadmap/reports=E2=86=9208?= =?UTF-8?q?c=20=E5=85=A8=E9=87=8F=C2=B7systems/tests-README/reports?= =?UTF-8?q?=E2=86=9208e=20=E5=85=A8=E9=87=8F=C2=B710=20=E6=81=92=E8=B7=91)?= =?UTF-8?q?,=E5=BF=AB=E8=B7=AF=E5=BE=84=E7=BA=A2=E5=BF=85=E5=85=A8?= =?UTF-8?q?=E9=87=8F=E5=A4=8D=E8=B7=91=E5=AE=9A=E8=B0=B3(=E6=B8=85?= =?UTF-8?q?=E5=8D=95=E9=9D=A2=E5=A4=96=E7=BA=A2=E4=B8=8D=E5=85=A5=E5=88=A4?= =?UTF-8?q?=E5=AE=9A=E9=9D=A2,=E4=B8=8E=20CI=20=E5=85=A8=E9=87=8F=E4=B8=A5?= =?UTF-8?q?=E6=A0=BC=E5=AF=B9=E9=BD=90),=E6=9C=AA=E8=A7=A6=E5=8F=91?= =?UTF-8?q?=E9=97=B8=E8=AE=B0=20SKIP=20=E5=85=A5=20not=5Frun;user=20?= =?UTF-8?q?=E7=9B=AE=E5=BD=95=E9=9A=94=E7=A6=BB=E6=94=B6=E7=AA=84=E8=87=B3?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E5=9F=9F(=E6=96=87=E6=A1=A3=E5=9F=9F?= =?UTF-8?q?=E4=B8=8D=E5=90=AF=E5=BC=95=E6=93=8E=E7=9C=81=200.8=20=E7=A7=92?= =?UTF-8?q?);pre-commit=20=E9=92=A9=E5=AD=90=E6=94=B6=E6=9A=82=E5=AD=98?= =?UTF-8?q?=E6=B8=85=E5=8D=95(quotepath=20=E5=85=B3=E9=97=AD=E4=BF=9D?= =?UTF-8?q?=E4=B8=AD=E6=96=87=E5=90=8D,NUL=20=E8=BD=AC=E8=A1=8C=E8=90=BD?= =?UTF-8?q?=E4=B8=B4=E6=97=B6=E6=96=87=E4=BB=B6)=E4=BC=A0=20--staged,?= =?UTF-8?q?=E6=B8=85=E5=8D=95=E5=BB=BA=E4=B8=8D=E8=B5=B7=E5=9B=9E=E8=90=BD?= =?UTF-8?q?=E5=85=A8=E9=87=8F=E6=A1=A3;=E4=B8=8D=E5=B8=A6=20--staged=3D?= =?UTF-8?q?=E7=8E=B0=E8=A1=8C=E5=85=A8=E9=87=8F=E6=A1=A3=20CI=20=E9=9B=B6?= =?UTF-8?q?=E5=BD=B1=E5=93=8D;=E7=AA=84=E6=B5=8B=E4=B8=89=E6=80=81?= =?UTF-8?q?=E8=BF=87(=E7=BA=AF=E4=BB=A3=E7=A0=81=E5=85=A8=20SKIP=206.2s/?= =?UTF-8?q?=E7=99=BB=E8=AE=B0=E9=9D=A2=2008c=20=E8=A7=A6=E5=8F=91+?= =?UTF-8?q?=E9=9D=A2=E5=A4=96=E7=BA=A2=E5=AE=9A=E8=B0=B3=E6=B3=A8=E8=AE=B0?= =?UTF-8?q?=207.9s/=E8=AE=BE=E8=AE=A1=E6=96=87=E6=A1=A3=E5=BF=AB=E8=B7=AF?= =?UTF-8?q?=E5=BE=84=207.3s),Windows=20=E7=93=B6=E9=A2=88=3Dpython=20?= =?UTF-8?q?=E8=BF=9B=E7=A8=8B=E5=90=AF=E5=8A=A8,=E5=AE=9E=E9=99=85?= =?UTF-8?q?=E7=9C=81=201-3=20=E7=A7=92;=E6=A8=AA=E5=B9=85=E9=92=A9?= =?UTF-8?q?=E5=AD=90=E4=B8=8E=20config=20=E6=B3=A8=E5=86=8C=E4=B8=8D?= =?UTF-8?q?=E5=85=A5=20git(.zcode=20gitignored)=E9=9A=8F=E7=9B=98=E7=94=9F?= =?UTF-8?q?=E6=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tools/hooks/pre-commit | 10 ++- tools/run_all_gates.sh | 134 +++++++++++++++++++++++++++++++++++------ 2 files changed, 123 insertions(+), 21 deletions(-) diff --git a/tools/hooks/pre-commit b/tools/hooks/pre-commit index a285c4ef..fa016191 100755 --- a/tools/hooks/pre-commit +++ b/tools/hooks/pre-commit @@ -12,7 +12,15 @@ REPO="$(git rev-parse --show-toplevel)" [ -f "$(git rev-parse --git-dir)/MERGE_HEAD" ] && exit 0 RC=0 -bash "$REPO/tools/run_all_gates.sh" --docs || RC=$? +# 收暂存清单(增量档输入):quotepath 关闭保中文名原样,NUL 转行落临时文件传闸机 +_LIST="$(mktemp "${TMPDIR:-/tmp}/gates-staged.XXXXXX")" || _LIST="" +if [ -n "$_LIST" ]; then + git -C "$REPO" -c core.quotepath=false diff --cached --name-only -z | tr '\0' '\n' >"$_LIST" + bash "$REPO/tools/run_all_gates.sh" --docs --staged "$_LIST" || RC=$? + rm -f "$_LIST" +else + bash "$REPO/tools/run_all_gates.sh" --docs || RC=$? +fi if [ "$RC" -eq 0 ]; then exit 0 elif [ "$RC" -eq 1 ]; then diff --git a/tools/run_all_gates.sh b/tools/run_all_gates.sh index 87252a50..226e4def 100644 --- a/tools/run_all_gates.sh +++ b/tools/run_all_gates.sh @@ -22,6 +22,13 @@ # ——本批改动只有文档件时用:game/ 内的 .md(README、生成清单)、design/、根规则文件、 # tools/ 工具脚本、.zcode/ 配置等一切非代码面改动。 # --quick 全量档的中段自检形态:跳过渲染闸(窗口化,需开显示器)与静态扫描;完成出库禁用。 +# --staged <清单文件> 文档域增量档(仅配 --docs):清单=本次提交暂存文件(pre-commit 钩子生成,NUL 转行)。 +# 触发矩阵(唯一落点):任一 .md → 06/06b/07/08/08d 先按清单内 .md 扫描(快路径); +# 待办清单.md/roadmap.md/reports/**.md 任一 → 08c 全量(关系型,不可切片); +# design/gdd/systems/**、game/tests/README.md、reports/**.md 任一 → 08e 全量(关系型); +# 10 熵增闸恒跑(结构哨兵)。快路径扫出红时全量复跑定谳——清单面外文件的失败不在全量档 +# 判定面内,全量复跑绿=按绿放行并注记,红=真红;增量判定面与 CI 全量档严格对齐, +# 跨文件漏检面(改 A 断 B 的链)由合回后 CI 全量兜底。缺省不带 --staged = 现行全量档,CI 零影响。 # 不带参数 = 全量档(两域全跑),触发条件=同批既有代码面改动又有文档件改动。闸件(tools/ 下机检脚本 # 与闸壳)自身的改动不占合回全量——其质量义务=落批时人工确认闸可跑通且判据口径未变; # 闸真崩溃时合回闸 fail-closed(跑不出回执即拦),不以全量为兜底。 @@ -47,22 +54,34 @@ BASELINE="$STAMP_DIR/baseline.json" QUICK=0 DOMAIN="full" -for _a in "$@"; do - case "$_a" in +STAGED_FILE="" +while [ $# -gt 0 ]; do + case "$1" 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 ;; + DOMAIN="${1#--}" ;; + --staged) + [ -n "${2:-}" ] || { echo "用法: --staged <暂存清单文件>"; exit 2; } + STAGED_FILE="$2"; shift ;; + *) echo "用法: tools/run_all_gates.sh [--code|--docs|--quick] [--staged <清单文件>]"; exit 2 ;; esac + shift done +if [ -n "$STAGED_FILE" ]; then + [ "$DOMAIN" = "docs" ] || { echo "用法: --staged 仅配 --docs(增量档只定义在文档域)"; exit 2; } + [ -f "$STAGED_FILE" ] || { echo "用法: --staged 清单文件缺位:$STAGED_FILE"; exit 2; } +fi # 每个代码面 worktree 用独立 Godot user 目录(game/override.cfg 由 setup 脚本自愈生成;主区 no-op)。 # 并行批共用一套夹具、日志与截图目录会互相踩盘造成假红——隔离建不起来就拒跑(fail-closed)。 -"$REPO/tools/setup_worktree_userdir.sh" "$REPO" || { - echo "[gates] FAIL — user 目录隔离未建立,拒绝在共享目录上跑闸。" - exit 1 -} +# 隔离只服务代码域(Godot 运行面);文档域不启引擎,跳过省 0.8 秒。 +if [ "$DOMAIN" = "code" ] || [ "$DOMAIN" = "full" ]; then + "$REPO/tools/setup_worktree_userdir.sh" "$REPO" || { + echo "[gates] FAIL — user 目录隔离未建立,拒绝在共享目录上跑闸。" + exit 1 + } +fi # 控制台编码:本仓库闸件输出中文与 ✅/✗ 符号。Windows GBK 控制台下 python 打印这些字符会抛 # UnicodeEncodeError——待办清单/10 两闸直接崩(假红)、09 的 FAIL 行被写成乱码字节(指纹漂移, @@ -338,6 +357,38 @@ gate() { # gate <名称> <日志名> <命令...> return 0 } +# 增量文档检查壳(--staged 档专用):先按暂存清单扫(快路径),扫出红时全量复跑定谳—— +# 清单面外文件的失败不在全量档判定面内,全量复跑绿=按绿放行并注记,红=真红。 +# 增量判定面与 CI 全量档严格对齐(增量档不产生全量档不会有的红)。 +gate_docs_inc() { # gate_docs_inc <名称> <日志名> <全量命令...> -- <增量命令...> + local name="$1" log="$2"; shift 2 + local -a full_cmd=() inc_cmd=() + local _in_inc=0 _a + for _a in "$@"; do + if [ "$_a" = "--" ]; then _in_inc=1; continue; fi + if [ $_in_inc -eq 0 ]; then full_cmd+=("$_a"); else inc_cmd+=("$_a"); fi + done + echo "" + echo "════════════════════════════════════════════" + echo " $name(增量档:暂存 .md $(( ${#inc_cmd[@]} - ${#full_cmd[@]} )) 件)" + echo "════════════════════════════════════════════" + local rc=0 + "${inc_cmd[@]}" >"$STAMP_DIR/$log" 2>&1 + rc=$? + if [ $rc -ne 0 ]; then + "${full_cmd[@]}" >>"$STAMP_DIR/$log" 2>&1 + rc=$? + if [ $rc -eq 0 ]; then + echo " [增量注记] 清单扫描出红、全量复跑绿——红项在暂存清单面外(不在全量档判定面内),按绿放行。" + fi + fi + 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 模式,退出码为唯一判据;分片态=多 Godot 进程各跑一片 local name="02 单元与集成测试" # 关联档选跑:把套件词列表经 --only= 传给 run_all(闸机来源标记 --only-source=gates, @@ -425,19 +476,62 @@ else gate "05 渲染闸" "render.log" bash "$REPO/tools/render_gate.sh" "$GAME" fi fi -# ══ 文档域(6–8c)══ +# ══ 文档域(6–8e)══ if in_domain docs; then -# ── 6. 文档三闸 ── -gate "06 gdd_lint" "gdd_lint.log" "$PY" "$REPO/tools/gdd_lint.py" -gate "06b 变更记录节" "changelog.log" "$PY" "$REPO/tools/gdd_lint.py" --changelog-only -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. 流水线闸(待办清单条目状态机+roadmap 迭代总表+一次交叉核,判据=doc-conventions §12)── -gate "08c 流水线闸" "pipeline.log" "$PY" "$REPO/tools/pipeline_gate.py" -# ── 8d. 引用可核(现行文档里「文件:行号」指针的目标存在性与行界,判据=件头注)── -gate "08d 引用可核" "doc_ref.log" "$PY" "$REPO/tools/doc_ref_audit.py" --strict -# ── 8e. 能力审计(GDD 纯度+能力清单与覆盖域表结构,判据=test-conventions §8.9+doc-conventions §5)── -gate "08e 能力审计" "capability.log" "$PY" "$REPO/tools/capability_audit.py" --strict + # ── 增量触发矩阵(唯一落点,判据依据与漏检面见头注 --staged 段)── + _md_list=() + _trigger_08c=0 + _trigger_08e=0 + if [ -n "$STAGED_FILE" ]; then + while IFS= read -r _f; do + [ -z "$_f" ] && continue + case "$_f" in + 待办清单.md|roadmap.md|reports/*) _trigger_08c=1 ;; + esac + case "$_f" in + design/gdd/systems/*|game/tests/README.md|reports/*) _trigger_08e=1 ;; + esac + case "$_f" in + *.md) [ -f "$REPO/$_f" ] && _md_list+=("$_f") ;; + esac + done < "$STAGED_FILE" + fi + if [ -z "$STAGED_FILE" ]; then + # 全量档(CI 与手动档原路径,判定面不变) + gate "06 gdd_lint" "gdd_lint.log" "$PY" "$REPO/tools/gdd_lint.py" + gate "06b 变更记录节" "changelog.log" "$PY" "$REPO/tools/gdd_lint.py" --changelog-only + 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 + gate "08c 流水线闸" "pipeline.log" "$PY" "$REPO/tools/pipeline_gate.py" + gate "08d 引用可核" "doc_ref.log" "$PY" "$REPO/tools/doc_ref_audit.py" --strict + gate "08e 能力审计" "capability.log" "$PY" "$REPO/tools/capability_audit.py" --strict + else + # 增量档:清单内 .md 走快路径(扫出红全量复跑定谳,见 gate_docs_inc) + if [ "${#_md_list[@]}" -gt 0 ]; then + gate_docs_inc "06 gdd_lint" "gdd_lint.log" "$PY" "$REPO/tools/gdd_lint.py" -- "$PY" "$REPO/tools/gdd_lint.py" "${_md_list[@]}" + gate_docs_inc "06b 变更记录节" "changelog.log" "$PY" "$REPO/tools/gdd_lint.py" --changelog-only -- "$PY" "$REPO/tools/gdd_lint.py" --changelog-only "${_md_list[@]}" + gate_docs_inc "07 doc_link" "doc_link.log" "$PY" "$REPO/tools/doc_link_audit.py" --strict -- "$PY" "$REPO/tools/doc_link_audit.py" --strict "${_md_list[@]}" + gate_docs_inc "08 doc_term" "doc_term.log" "$PY" "$REPO/tools/doc_term_audit.py" --strict -- "$PY" "$REPO/tools/doc_term_audit.py" --strict "${_md_list[@]}" + gate_docs_inc "08d 引用可核" "doc_ref.log" "$PY" "$REPO/tools/doc_ref_audit.py" --strict -- "$PY" "$REPO/tools/doc_ref_audit.py" --strict "${_md_list[@]}" + else + for _n in "06 gdd_lint" "06b 变更记录节" "07 doc_link" "08 doc_term" "08d 引用可核"; do + NAMES+=("$_n"); EXITS+=("SKIP"); LOGS+=("") + done + echo ""; echo "[gates] 06/06b/07/08/08d → SKIP(增量档:暂存清单无 .md)" + fi + if [ "$_trigger_08c" -eq 1 ]; then + gate "08c 流水线闸" "pipeline.log" "$PY" "$REPO/tools/pipeline_gate.py" + else + NAMES+=("08c 流水线闸"); EXITS+=("SKIP"); LOGS+=("") + echo ""; echo "[gates] 08c 流水线闸 → SKIP(增量档:登记面文件未触)" + fi + if [ "$_trigger_08e" -eq 1 ]; then + gate "08e 能力审计" "capability.log" "$PY" "$REPO/tools/capability_audit.py" --strict + else + NAMES+=("08e 能力审计"); EXITS+=("SKIP"); LOGS+=("") + echo ""; echo "[gates] 08e 能力审计 → SKIP(增量档:系统文档/能力清单/卡片未触)" + fi + fi fi # ── 7. 静态扫描(代码域)+ 熵增闸(两域都跑:它查顶层目录/根文件/图像白名单/退役路径,