Files
gplan-2/game/tests/static_scan.sh
T
DKChan 811f988032 rules(规则审阅落地批): 十条评审意见落地——呈报四问/文件头缩窄/禁词表冻结/lean死条文退役/日期批注清除/AGENTS瘦身/分域闸口径/状态栏纯化
- AGENTS.md: 呈报六问改读后自检四问;文件头红线删行数上限与白名单;日期批注清除;回执五况细则挪 process-conventions §3.10;流水线闸过时状态句改事实句
- process-conventions: §1.2 非迭代批改按改动域跑闸;§3.10 承接五况;两处日期批注清除;§2 表格凭据落点改现状清单
- code-conventions: §1.4.1 重写(删 8 行上限/两类白名单/90 文件清偿义务);§2.1 联动;两处日期批注清除
- test-conventions: 三处日期批注清除(三处实证日期属证据保留)
- doc-conventions: §1 lean 死条文删除;§4 词表冻结条款;§12.1 重写(状态行只写状态词;现状=关联文档清单;删括号凭据全部条款);三处日期批注清除;§5 重复词修复
- 术语表: 用语规范节加词表冻结句
- 06-待办清单: 11 条带括号状态行迁移(括号证据移入现状关联文档清单;待裁决说明并入现状;每条状态行只留状态词)
- tools/audit_file_headers.py: 删 R1 行数上限(R0 载体/R2 日期/R3 变更叙事保留)
- tools/gdd_lint.py: R2 表格占比判据与 R2_EXEMPT 整体退役(lean 解读对象已亡,只产存量噪音)
- tools/doc_term_audit.py: 词表冻结注释
- tools/pipeline_gate.py: 状态栏括号凭据核对废止,新增状态行禁括号判据(点名病因短路);闭合证据改现状正文核对(卡片路径磁盘存在性+交叉核、文档路径+版本、art-bible+版本)
- game/tests/static_scan.sh: S10 注释同步

变异验证(test-conventions §6):pipeline_gate 注入括号→状态行含括号内容红 exit1;删正文卡片路径→正文缺卡片路径+交叉核红 exit1;audit_file_headers 注入变更叙事→R2/R3 红 exit1、头注 12 行纯说明→0 红(上限停用实证)。坑册新增:未提交工作区禁 git checkout 变异还原(会连带回滚在途改动),须备份文件法。

全批净变化 +155/−146(12 文件)。
2026-10-20 23:15:05 +08:00

214 lines
11 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
# tests/static_scan.sh — 静态扫描域 S1–S8 可执行子集
# 规则全集与动机:design/engineering/architecture/architecture.md「测试与质量域」节
# 正式工程(core/ 存在)时执行;否则优雅跳过(工程在 game/core——仓库根布局)。
# 纪律:新违规案例必须先补本脚本规则再修码(design/engineering/code-conventions.md 缺陷-规范对照表)。
set -u
cd "$(dirname "$0")/../.." # S 闸规则路径均为仓根相对(落点错会致 game/core 守卫恒跳过=静默空转)
if [ ! -d game/core ]; then
echo "[static_scan] game/core 不存在(工程未创建)——跳过,退出 0"
exit 0
fi
fail=0
# ── 扫描路径单点 + 零命中自检 ──
# 静默空转比红更危险:S3 曾把路径写成裸 `core`(仓根无此目录,实际是 game/core),
# 恒零命中恒绿、什么都不扫,直到本批才被发现。故每条规则的扫描路径必须是单点变量,
# 且规则自己先数一遍扫描面文件数——数为零一律判红(这样路径写歪时自检会跟着一起歪、一起红)。
DIR_CORE="game/core"
DIR_PRES="game/presentation"
scan_files() { # scan_files <目录> — 该目录下 .gd 文件数
find "$1" -name '*.gd' 2>/dev/null | wc -l
}
require_scan_face() { # require_scan_face <规则名> <目录> — 扫描面为空即判红
local n
n=$(scan_files "$2")
if [ "$n" -eq 0 ]; then
echo "[static_scan] $1 FAIL: 扫描面为空——$2 下零个 .gd(规则路径写错导致的静默空转)"
fail=1
return 1
fi
return 0
}
# S9 — 类型闸(code-conventions §4 机器闸:产品码无类型容器/有值返回缺类型/参数缺类型)
if python tools/audit_type_hints.py; then
echo "[static_scan] S9 ok"
else
echo "[static_scan] S9 FAIL: 类型闸存量未归零(R1/R2/R3 见上清单)"
fail=1
fi
# S10 — 文件头闸(code-conventions §1.4.1:## 载体+日期/变更叙事黑名单;行数上限已停用)
if python tools/audit_file_headers.py; then
echo "[static_scan] S10 ok"
else
echo "[static_scan] S10 FAIL: 文件头存量见上清单"
fail=1
fi
# S1/S2 — 玩法代码禁 randomize()/自建 RandomNumberGenerator/全局随机函数(绕开 RngService 的口子)
# 违规签名=裸 randomize/rand* 调用(实例方法 .randi() 排除)
# + RngService 之外的 RandomNumberGenerator.new();接收服务注入流的类型标注不属违规(core 五文件 15 处实证)
if require_scan_face "S1/S2" "$DIR_CORE"; then
hits=$(grep -rnE '\brandomize[[:space:]]*\(|(^|[^.[:alnum:]_])randi[[:space:]]*\(|(^|[^.[:alnum:]_])randf[[:space:]]*\(|(^|[^.[:alnum:]_])randf_range[[:space:]]*\(' \
"$DIR_CORE" "$DIR_PRES" --include='*.gd' 2>/dev/null | grep -vE '^[^:]+:[0-9]+:[[:space:]]*#' | grep -v 'rng-seed-exempt' | wc -l)
hits2=$(grep -rn 'RandomNumberGenerator\.new()' "$DIR_CORE" "$DIR_PRES" --include='*.gd' 2>/dev/null | grep -vc rng_service)
if [ "$hits" -gt 0 ] || [ "$hits2" -gt 0 ]; then echo "[static_scan] S1/S2 FAIL: 全局随机/RNG 自建命中 $hits+$hits2 处"; grep -rnE '\brandomize[[:space:]]*\(|(^|[^.[:alnum:]_])randi[[:space:]]*\(|(^|[^.[:alnum:]_])randf[[:space:]]*\(|(^|[^.[:alnum:]_])randf_range[[:space:]]*\(' "$DIR_CORE" "$DIR_PRES" --include='*.gd' 2>/dev/null; grep -rn 'RandomNumberGenerator\.new()' "$DIR_CORE" "$DIR_PRES" --include='*.gd' 2>/dev/null | grep -v rng_service; fail=1; else echo "[static_scan] S1/S2 ok"; fi
fi
# S4 — core/ 禁 extends Control/Node2D、禁 .tscn preload(headless 债)
if require_scan_face "S4" "$DIR_CORE"; then
hits=$(grep -rnE 'extends[[:space:]]+(Control|Node2D)\b|\.tscn' "$DIR_CORE" --include='*.gd' 2>/dev/null | wc -l)
if [ "$hits" -gt 0 ]; then echo "[static_scan] S4 FAIL: core 引擎/UI 依赖命中 $hits 处"; grep -rnE 'extends[[:space:]]+(Control|Node2D)\b|\.tscn' "$DIR_CORE" --include='*.gd' 2>/dev/null; fail=1; else echo "[static_scan] S4 ok"; fi
fi
# S3 — core/ 禁 ConfigDB 外 data 表加载(schema class_name 直引豁免)
# 扫描路径必须写 game/core——写裸 `core` 仓根无此目录,恒零命中恒绿(静默空转)。
# 豁免:①注释行 ②game/core/config_db.gd 自身(数值单一事实源的现行事实源,它的单点 load 是本规则的例外)。
if require_scan_face "S3" "$DIR_CORE"; then
hits=$(grep -rnE '\b(load|preload)[[:space:]]*\(|ResourceLoader\.load' "$DIR_CORE" --include='*.gd' 2>/dev/null \
| grep -vE '^[^:]+:[0-9]+:[[:space:]]*#' \
| grep -v "$DIR_CORE/config_db.gd" \
| grep -v 'res://tests/' | wc -l)
if [ "$hits" -gt 0 ]; then echo "[static_scan] S3 WARNING(人工复核——class_name 直引误报需逐条排除): $hits 处"; grep -rnE '\b(load|preload)[[:space:]]*\(|ResourceLoader\.load' "$DIR_CORE" --include='*.gd' 2>/dev/null | grep -vE '^[^:]+:[0-9]+:[[:space:]]*#' | grep -v "$DIR_CORE/config_db.gd"; else echo "[static_scan] S3 ok(扫描面 $DIR_CORE 共 $(scan_files "$DIR_CORE") 个 .gd,非静默空转)"; fi
fi
# S5 — core/ 禁裸引用 autoload 全局标识符(ConfigDB / RngService 直接写 = --script 模式 parse error)
# 形态:标识符后紧跟 `.`(成员访问)才算裸引用;排除注释行,并排除被引号/斜杠包裹的字符串字面量
# (如 "[ConfigDB/%s]" 日志前缀、"/root/ConfigDB" 取节点路径)——那是文案与路径,不是标识符引用。
if require_scan_face "S5" "$DIR_CORE"; then
hits=$(grep -rnE '(^|[^.[:alnum:]_"/])(ConfigDB|RngService)[[:space:]]*\.' "$DIR_CORE" --include='*.gd' 2>/dev/null \
| grep -vE '^[^:]+:[0-9]+:[[:space:]]*#' | wc -l)
if [ "$hits" -gt 0 ]; then echo "[static_scan] S5 FAIL: core 裸引用 autoload 全局标识符命中 $hits 处(--script 模式 parse error)"; grep -rnE '(^|[^.[:alnum:]_"/])(ConfigDB|RngService)[[:space:]]*\.' "$DIR_CORE" --include='*.gd' 2>/dev/null | grep -vE '^[^:]+:[0-9]+:[[:space:]]*#'; fail=1; else echo "[static_scan] S5 ok(扫描面 $DIR_CORE 共 $(scan_files "$DIR_CORE") 个 .gd)"; fi
fi
# TODO(正式工程落地后精化为扫描配置并升格为硬失败):
# S6 core↔core 信号连接只许方法 Callable(禁 lambda 捕获 self);发射者禁反向引用
# S7 B2/B3/B4 API 符号命中文件 ⊆ B1 文件集(编排唯一性)
# S8 结算路径 await 零命中(B1 结算/编排文件集)
# ── S11 — 代码注释禁用词 ──
# 禁「板级」「门票」「正主」「落籍」「完美排雷」等退役词进注释面(完整词表见术语表解析)
# 权威出处:design/gdd/术语表.md「退役词对照」节+「对外命名纪律」节
# 扫描面:game/ 全域 .gd/.tres/.md/.json/.sh(.txt 不收=第三方许可证非本仓库文本)
# 词表来源:解析「退役词对照」第一列(去括号注、斜杠拆分)+显式追加禁「完美排雷」(受对外命名纪律约束,不在退役词对照表里);显式排除「巢穴」(玩家专名/风味层,术语表自带除外注)、「旗」(单字误报率高,doc_term_audit 判例)
# 注释文本提取:.gd/.sh 取 # 后文本;.tres 取 ; 后文本;.md 取整行;.json 无注释语法(保留扩展名但不产生注释段)
# 三层豁免依序:①闸门自指(行内含检查/禁令形态即跳过)②显式留档豁免单 4 处 ③非注释面天然不取
if [ -d game/core ]; then
S11_TABLE="design/gdd/术语表.md"
S11_FAIL_LOCAL=0
if [ ! -f "$S11_TABLE" ]; then
echo "[static_scan] S11 FAIL: 术语表文件不存在($S11_TABLE)"
S11_FAIL_LOCAL=1
else
# 解析词表:「退役词对照」第一列→去括号注→斜杠拆分
S11_WFILE=$(mktemp)
awk '/^## 退役词对照/{f=1;next} f && /^## /{exit} f && /^\|/ && !/^\|---/ && !/退役词/{
sub(/^\| */, ""); sub(/ *\|.*/, "")
gsub(/([^)]*)/, ""); gsub(/\([^)]*\)/, "")
gsub(/^ *| *$/, "")
n = split($0, parts, " / ")
for (i=1;i<=n;i++){gsub(/^ *| *$/,"",parts[i]); if(parts[i]!="") print parts[i]}
}' "$S11_TABLE" > "$S11_WFILE"
# 显式追加+排除
printf '完美排雷\n' >> "$S11_WFILE"
grep -vx '巢穴' "$S11_WFILE" | grep -vx '旗' | grep -vx 'flag_\*' | sort -u > "${S11_WFILE}.x" && mv "${S11_WFILE}.x" "$S11_WFILE"
# 自检二:词表解析非空
S11_WC=$(grep -c . "$S11_WFILE" 2>/dev/null || echo 0)
if [ "$S11_WC" -eq 0 ]; then
echo "[static_scan] S11 FAIL: 词表解析为空——术语表结构可能已变"
S11_FAIL_LOCAL=1
fi
# 自检一:扫描面非空
S11_FC=$(find game/ -type f \( -name '*.gd' -o -name '*.tres' -o -name '*.md' -o -name '*.json' -o -name '*.sh' \) 2>/dev/null | wc -l)
if [ "$S11_FC" -eq 0 ]; then
echo "[static_scan] S11 FAIL: 扫描面为空——路径写错导致的静默空转"
S11_FAIL_LOCAL=1
fi
if [ "$S11_FAIL_LOCAL" -eq 0 ]; then
# 扫描三个文件类型并合并结果
S11_ALL_HITS=$(
# .gd/.sh:取 # 后注释
find game/ -type f \( -name '*.gd' -o -name '*.sh' \) -print0 2>/dev/null \
| xargs -0 awk -v wf="$S11_WFILE" '
BEGIN{nw=0;while((getline w<wf)>0)words[++nw]=w;close(wf)}
/#/{
cs=index($0,"#"); cmt=substr($0,cs+1)
for(i=1;i<=nw;i++){
if(index(cmt,words[i])>0){
if($0~/contains\(|find\(|grep|has_char\(|禁「|禁『|零「|零『/)next
if(FILENAME~"enemy.tres"&&FNR==653)next
if(FILENAME~"level_runner"&&FNR==832)next
if(FILENAME~"adventure_runner"&&FNR==403)next
if(FILENAME~"damage_math_test"&&FNR==36)next
print FILENAME":"FNR":"words[i]
next
}
}
}
' 2>/dev/null
# .tres:取 ; 后注释
find game/ -type f -name '*.tres' -print0 2>/dev/null \
| xargs -0 awk -v wf="$S11_WFILE" '
BEGIN{nw=0;while((getline w<wf)>0)words[++nw]=w;close(wf)}
/;/{
cs=index($0,";"); cmt=substr($0,cs+1)
for(i=1;i<=nw;i++){
if(index(cmt,words[i])>0){
if($0~/contains\(|find\(|grep|has_char\(|禁「|禁『|零「|零『/)next
if(FILENAME~"enemy.tres"&&FNR==653)next
print FILENAME":"FNR":"words[i]
next
}
}
}
' 2>/dev/null
# .md:整行即注释面
find game/ -type f -name '*.md' -print0 2>/dev/null \
| xargs -0 awk -v wf="$S11_WFILE" '
BEGIN{nw=0;while((getline w<wf)>0)words[++nw]=w;close(wf)}
{
for(i=1;i<=nw;i++){
if(index($0,words[i])>0){
if($0~/contains\(|find\(|grep|has_char\(|禁「|禁『|零「|零『/)next
print FILENAME":"FNR":"words[i]
next
}
}
}
' 2>/dev/null
)
S11_HITS=0
if [ -n "$S11_ALL_HITS" ]; then
S11_HITS=$(echo "$S11_ALL_HITS" | grep -c .)
fi
if [ "$S11_HITS" -gt 0 ]; then
echo "[static_scan] S11 FAIL: 代码注释禁用词命中 $S11_HITS 处"
echo "$S11_ALL_HITS" | head -50
if [ "$S11_HITS" -gt 50 ]; then echo " ...(截断,共 $S11_HITS 处)"; fi
fail=1
else
echo "[static_scan] S11 ok(扫描面 $S11_FC 个文件,词表 $S11_WC 词)"
fi
else
fail=1
fi
rm -f "$S11_WFILE"
fi
fi
exit $fail