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. 静态扫描(代码域)+ 熵增闸(两域都跑:它查顶层目录/根文件/图像白名单/退役路径,