405 lines
25 KiB
Bash
Executable File
405 lines
25 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# tests/static_scan.sh — 静态扫描域 S1–S10 全量在位
|
||
# 规则全集与动机: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
|
||
|
||
# ── S6 — core 信号连接禁内联 lambda(架构文档 S 表:core↔core 信号连接只许方法 Callable,禁 lambda 捕获 self)──
|
||
# 扫描面:$DIR_CORE(game/core,core↔core 规则域=core 文件集;单点变量防空转)。
|
||
# 判据=信号连接的内联 lambda 形态,保守集合(误报零容忍,拿不准的形态宁可不抓不红):
|
||
# ①同行形态:`.connect(` 后紧接 `func`——`sig.connect(func(a): ...)`;
|
||
# ②跨行形态:行尾以 `.connect(` 结束、下一行以 `func(` 起始(awk 前行状态机实现)。
|
||
# 拿不准不抓的形态:bind/Callable 变量中转等间接 lambda 通道、注释外的字符串字面量内 `.connect(func` 同行
|
||
# 全行注释已排除、行尾注记已豁免——间接通道归人工评审,本闸只防直接内联 lambda 回归。
|
||
# 豁免通道:行内注记 s6-signal-exempt(仿 S2 rng-seed-exempt)——确需内联 lambda 的在册例外逐处留档。
|
||
# 规则第二句「发射者禁持有订阅者反向引用」:静态判据不成熟(持有引用形态难可靠识别),不造弱判据,
|
||
# 该句归人工评审面承接,本闸只防内联 lambda 回归(2026-10-24 人工 grep 盘面干净,闸为防回归防线)。
|
||
if require_scan_face "S6" "$DIR_CORE"; then
|
||
S6_HITS="$(
|
||
grep -rnE '\.connect[[:space:]]*\([[:space:]]*func\b' "$DIR_CORE" --include='*.gd' 2>/dev/null \
|
||
| grep -vE '^[^:]+:[0-9]+:[[:space:]]*#' | grep -v 's6-signal-exempt';
|
||
find "$DIR_CORE" -name '*.gd' -print0 2>/dev/null | xargs -0 awk '
|
||
prev ~ /\.connect[ \t]*\([ \t]*$/ && $0 ~ /^[ \t]*func[ \t]*\(/ &&
|
||
prev !~ /^[ \t]*#/ && prev !~ /s6-signal-exempt/ && $0 !~ /s6-signal-exempt/ {
|
||
print FILENAME ":" FNR ":" $0 " (跨行 lambda——上一行行尾 .connect( )"
|
||
}
|
||
{ prev = $0 }'
|
||
)"
|
||
if [ -n "$S6_HITS" ]; then
|
||
echo "[static_scan] S6 FAIL: core 信号连接内联 lambda 命中(只许方法 Callable,禁 lambda 捕获 self)"
|
||
echo "$S6_HITS"
|
||
fail=1
|
||
else
|
||
echo "[static_scan] S6 ok(扫描面 $DIR_CORE 共 $(scan_files "$DIR_CORE") 个 .gd,内联 lambda 零命中)"
|
||
fi
|
||
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
|
||
|
||
|
||
# ── S11 — 代码注释禁用词 ──
|
||
# 禁「板级」「门票」「正主」「落籍」「完美排雷」等退役词进注释面(完整词表见术语表解析)
|
||
# 权威出处:design/gdd/术语表.md「退役词对照」节+「对外命名纪律」节
|
||
# 同形异义豁免:机制词「去重闸」(封印重判去重的玩法判别器)与某闸档退役词后二字同形——
|
||
# 行内含「去重闸」即整行跳过(2026-10-22 测试术语重构批随闸档用语入表增设,否则 adventure_feats_test.gd
|
||
# 在册断言标签成片误红;代价=该行同时出现的真闸档用词也不报,靠文档域 08 闸兜住)
|
||
# 扫描面:game/ 全域 .gd/.tres/.md/.json/.sh(.txt 不收=第三方许可证非本仓库文本)
|
||
# 注释文本提取:.gd/.sh 取 # 后文本;.tres 取 ; 后文本;.md 取整行;.json 无注释语法(保留扩展名但不产生注释段)
|
||
# 三层豁免依序:①闸门自指(行内含检查/禁令形态即跳过)②显式留档豁免单 20 处 ③非注释面天然不取
|
||
# 豁免单新增三处(2026-10-26 令牌配额口径清扫批 B4b 用户裁定保留):adventure_table.gd:26 档名映射对照注
|
||
# +adventure_table.gd:93 与 curio_deck.gd:130 四口径契约④原名引述——合法载体非残留,改写即断链
|
||
# 豁免单再增 13 处(2026-10-26 口径换代批用户裁「带日期历史读数注释保留原文」):
|
||
# hero.tres:21/:36、enemy.tres:751/:891、action_economy.tres:15/:16/:26/:27、status_effects.tres:91、
|
||
# meta.tres:48/:56/:62(.tres 十二处历史读数/带日期批记录)+level_autoplay.gd:560(完美档 41% 历史实证)——
|
||
# 历史读数原文锚定当时实测口径,改写即失真;现行口径描述不豁免(已随批换代)
|
||
|
||
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"
|
||
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(index(cmt,"去重闸")>0)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
|
||
if(FILENAME~"adventure_table"&&FNR==26)next
|
||
if(FILENAME~"adventure_table"&&FNR==93)next
|
||
if(FILENAME~"curio_deck"&&FNR==130)next
|
||
if(FILENAME~"level_autoplay"&&FNR==560)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(index(cmt,"去重闸")>0)next
|
||
if(FILENAME~"enemy.tres"&&FNR==653)next
|
||
if(FILENAME~"hero.tres"&&FNR==21)next
|
||
if(FILENAME~"hero.tres"&&FNR==36)next
|
||
if(FILENAME~"enemy.tres"&&FNR==751)next
|
||
if(FILENAME~"enemy.tres"&&FNR==891)next
|
||
if(FILENAME~"action_economy.tres"&&FNR==15)next
|
||
if(FILENAME~"action_economy.tres"&&FNR==16)next
|
||
if(FILENAME~"action_economy.tres"&&FNR==26)next
|
||
if(FILENAME~"action_economy.tres"&&FNR==27)next
|
||
if(FILENAME~"status_effects.tres"&&FNR==91)next
|
||
if(FILENAME~"meta.tres"&&FNR==48)next
|
||
if(FILENAME~"meta.tres"&&FNR==56)next
|
||
if(FILENAME~"meta.tres"&&FNR==62)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
|
||
if(index($0,"去重闸")>0)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
|
||
|
||
# ── S12 — 效果引擎无状态化旧件零残留(卡② V10/F1;禁 counters_export/counters_seed/_reset_tree_limited_counters 等旧符号与带引号旧 id 字面回归)──
|
||
# 扫描面:game/ 全域 .gd 与 game/data/tables/*.tres(改名与旧符号同时存在于数据行/测试/smoke,非仅 core——不扩面即空转)
|
||
# 带引号旧 id 匹配("twin_digs" 不误伤 "adv_twin_digs"——引号锚定;round_instinct_atk 不匹 "instinct_atk")
|
||
# 自检:扫描面文件数为零即判红(防静默空转)
|
||
if [ -d game/core ]; then
|
||
S12_SYM='counters_export|counters_seed|counters_merge_export|counters_seed_snapshot|_reset_tree_limited_counters|"tree_sense_sharp_used"|"tree_sense_guard_used"|"twin_digs"|"instinct_atk"'
|
||
S12_FACE=$(find game/ -type f \( -name '*.gd' -o -name '*.tres' \) 2>/dev/null)
|
||
S12_FC=$(echo "$S12_FACE" | grep -c .)
|
||
if [ "$S12_FC" -eq 0 ]; then
|
||
echo "[static_scan] S12 FAIL: 扫描面为空——路径写错导致的静默空转"
|
||
fail=1
|
||
else
|
||
S12_HITS=$(echo "$S12_FACE" | tr '\n' '\0' | xargs -0 grep -lEn "$S12_SYM" 2>/dev/null | wc -l)
|
||
if [ "$S12_HITS" -gt 0 ]; then
|
||
echo "[static_scan] S12 FAIL: 无状态化旧件残留命中 $S12_HITS 文件"
|
||
echo "$S12_FACE" | tr '\n' '\0' | xargs -0 grep -En "$S12_SYM" 2>/dev/null
|
||
fail=1
|
||
else
|
||
echo "[static_scan] S12 ok(扫描面 $S12_FC 个 .gd/.tres,旧件零残留)〔无状态化 V10〕"
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
# ── S13 — 第三方质量规则闸(gdstyle v0.4.0,版本钉死)──
|
||
# 规则说明自持本头注(随 S11/S12 惯例):gdstyle(github.com/atelico/gdstyle,Rust 单二进制)只开
|
||
# quality/* 抓真缺陷(重复 load/循环内 await/循环内分配/不可达代码/自比较/字典重复键/残留
|
||
# print 等),format/*/naming/*/order/* 与 S9/S10 职能重叠全关;扫描面=产品码三目录(与 S9 同口径,
|
||
# probe/sim/tests 测量面直构惯例不在面)。存量基线 tools/gdstyle_baseline.txt 用稳定身份键(函数级带数
|
||
# 条目=文件|规则|函数名,类级/文件级带数条目=文件|规则,均带登记数值;无数字条目=文件|规则|消息全文
|
||
# 裸键)——消息里的行数/分支数等计数漂移不假红;判定=基线外新违规红(NEW)+同键指标较登记值恶化红
|
||
# (WORSENED),改小/持平不红;逐条违规行带 FAIL 前缀,使存量红期间的红项清单(哪几条、恶化到多少)
|
||
# 进 gate 09 指纹,新增或进一步恶化不会被「同款存量红」放过。阈值口径=gdstyle 默认值;基线只是欠账
|
||
# 簿,登记值只准随还债下降,禁抬高到现状。基线重生成=人工专用(python tools/gdstyle_gate.py
|
||
# --refresh,附对账;键数骤降逾 10% 或登记值被抬高均拒写,须 --force),禁清基线换绿。二进制本机固定路径
|
||
# D:/code/gdstyle/gdstyle.exe(不入库),GDSTYLE_BIN 环境变量覆盖;版本与钉定版不符即红(升级须重测)。
|
||
if python tools/gdstyle_gate.py; then
|
||
echo "[static_scan] S13 ok"
|
||
else
|
||
echo "[static_scan] S13 FAIL: 基线外新违规/指标恶化或二进制/版本/扫描面问题(清单见上)"
|
||
fail=1
|
||
fi
|
||
|
||
# ── S8 — 结算路径 await 零命中(B1 结算/编排文件集)──
|
||
# 规则(architecture.md「静态扫描域 S1–S8 承接清单」S8 行 / D2 AC-14 口径):结算本体是
|
||
# 可回放纯逻辑步骤流(同种子同输入逐位复现),await 会打断确定性——「模块归属」B1 行已写明
|
||
# 「禁 await——切片 88 冒烟实证」。本闸为防回归防线(2026-10-24 人工 grep 盘面干净)。
|
||
# 扫描面:B1 域文件集(与 architecture.md「模块归属」B1 行同口径)=battle_orchestrator.gd
|
||
# + battle_fx_host.gd + reward_grant.gd + board_installer.gd 三拆件 + settlement/ 五段段类 + damage_controller.gd,共 14 文件,显式清单见 S8_FILES。
|
||
# 执行侧字面值豁免:脚本必须带字面值才能工作;文件集与架构文档冲突时以 B1 域文件集为准。
|
||
# 判据:文件集内出现 await 关键字即红(词边界 \bawait\b,任何形态——await 信号/协程/
|
||
# get_tree());排除注释行(行首 #/##)与字符串字面量(双引号包裹段内的 await 非代码),
|
||
# 口径仿 S1/S2 的 grep 排除手法。
|
||
# 自检:清单内文件缺位(路径写错/被改名)即判红——防扫面为空静默绿。
|
||
S8_FILES="game/core/battle_orchestrator.gd game/core/battle_fx_host.gd game/core/reward_grant.gd game/core/board_installer.gd game/core/settlement/aura_segment.gd game/core/settlement/combat_segment.gd game/core/settlement/decay_segment.gd game/core/settlement/grant_segment.gd game/core/settlement/verdict_segment.gd game/core/settlement/round_cycle.gd game/core/damage_controller.gd game/core/settlement/battle_refresh_flow.gd game/core/effect_port.gd game/core/reward_roller.gd"
|
||
S8_MISS=0
|
||
for f in $S8_FILES; do
|
||
if [ ! -f "$f" ]; then
|
||
echo "[static_scan] S8 FAIL: 文件集缺位——$f 不存在(路径写错/被改名导致的静默空转)"
|
||
S8_MISS=1
|
||
fi
|
||
done
|
||
if [ "$S8_MISS" -eq 1 ]; then
|
||
fail=1
|
||
else
|
||
S8_HITS=$(grep -rnE '\bawait\b' $S8_FILES 2>/dev/null \
|
||
| grep -vE '^[^:]+:[0-9]+:[[:space:]]*#' \
|
||
| grep -vE '"[^"]*\bawait\b[^"]*"' \
|
||
| wc -l)
|
||
if [ "$S8_HITS" -gt 0 ]; then
|
||
echo "[static_scan] S8 FAIL: 结算路径 await 命中 $S8_HITS 处(同种子逐位回放不可被 await 打断)"
|
||
grep -rnE '\bawait\b' $S8_FILES 2>/dev/null | grep -vE '^[^:]+:[0-9]+:[[:space:]]*#' | grep -vE '"[^"]*\bawait\b[^"]*"'
|
||
fail=1
|
||
else
|
||
echo "[static_scan] S8 ok(扫描面 B1 文件集 10 个 .gd,await 零命中)"
|
||
fi
|
||
fi
|
||
|
||
# ── S7 — B1 编排唯一性:B2/B3/B4 API 符号命中文件 ⊆ B1 域文件集 ──
|
||
# 规则(architecture.md「静态扫描域 S1–S8 承接清单」S7 行 / B1 AC-43;依赖约束节同款):
|
||
# B2/B3/B4 的对外 API 只允许被 B1 域文件集调用——全调用面集中 B1、禁第二调用方。
|
||
# 附带覆盖「A2/B3/B4 零相互直调」:B2/B3/B4 自身文件 ∉ B1 文件集,命中彼家符号即红
|
||
# (同族自调用豁免只放自家符号,彼家符号照红)。
|
||
# 符号清单权威源:architecture.md「模块归属」各表「对外暴露」列——B2(unit_container)14 符号、
|
||
# B3(status_container)5 符号、B4(reward_economy)9 符号;脚本字面值与文档冲突时以文档为准
|
||
# (执行侧字面值豁免,同 S8 手法)。B4 表内 is_mapped 静默布尔投影注明批量工具专用、不在暴露面清单。
|
||
# 扫描面:$DIR_CORE(含 settlement/,B1 文件集在其中)。
|
||
# 判据:文件内出现 `.符号(` 调用形态(符号后紧跟括号,天然词边界;`.my_grant(` 不误伤 `grant`)
|
||
# 且该文件 ∉ B1 文件集 → 红。排除注释行与字符串字面量(口径仿 S1/S2/S8 的 grep 排除手法)。
|
||
# 同族自调用豁免:unit_container/status_container/reward_economy 自身文件命中自家符号不算
|
||
# (容器内部互调合法;GDScript 裸调用无 `.` 本就不命中,带 self. 的自家调用由本条兜住)。
|
||
# 误报零容忍:spawn/heal/phase/grant 等通用名撞车同名异对象方法属误报——逐条判真伪后收紧判据
|
||
# 或加行内豁免注记 `s7-b1-exempt`(仿 rng-seed-exempt 惯例),禁放宽符号清单换绿。
|
||
# 自检:扫描面为空即判红(require_scan_face);B1 文件集与三族自家文件缺位即判红
|
||
# (文件被改名=豁免名单失效,宁可红不可静默放行)。
|
||
if require_scan_face "S7" "$DIR_CORE"; then
|
||
# B1 域文件集(迭代 46 卡A 增补 battle_refresh_flow——换图刷新流程件=C13 拆分边界的 B1 域拆件,
|
||
# 承载旧资源退役/装板编排调用;architecture.md「模块归属」BattleRefreshFlow 行随反哺点落文)
|
||
S7_B1_SET="game/core/battle_orchestrator.gd game/core/battle_fx_host.gd game/core/reward_grant.gd game/core/board_installer.gd game/core/settlement/aura_segment.gd game/core/settlement/combat_segment.gd game/core/settlement/decay_segment.gd game/core/settlement/grant_segment.gd game/core/settlement/verdict_segment.gd game/core/settlement/round_cycle.gd game/core/damage_controller.gd game/core/settlement/battle_refresh_flow.gd game/core/effect_port.gd game/core/reward_roller.gd"
|
||
S7_MISS=0
|
||
for f in $S7_B1_SET game/core/unit_container.gd game/core/status_container.gd game/core/reward_economy.gd; do
|
||
if [ ! -f "$f" ]; then
|
||
echo "[static_scan] S7 FAIL: 豁免名单缺位——$f 不存在(路径写错/被改名导致的静默空转)"
|
||
S7_MISS=1
|
||
fi
|
||
done
|
||
if [ "$S7_MISS" -eq 1 ]; then
|
||
fail=1
|
||
else
|
||
S7_HITS=""
|
||
s7_scan() { # s7_scan <族名> <自家文件> <符号交替式> — 扫 core 面,豁免 B1 文件集与自家文件
|
||
local grp="$1" own="$2" syms="$3" f h
|
||
while IFS= read -r f; do
|
||
case " $S7_B1_SET " in *" $f "*) continue ;; esac
|
||
[ "$f" = "$own" ] && continue
|
||
h=$(grep -nE "\.($syms)[[:space:]]*\(" "$f" 2>/dev/null \
|
||
| grep -vE '^[0-9]+:[[:space:]]*#' \
|
||
| grep -vE "\"[^\"]*\\.($syms)[[:space:]]*\\([^\"]*\"" \
|
||
| grep -v 's7-b1-exempt' \
|
||
| sed "s|^|[$grp] $f:|")
|
||
[ -n "$h" ] && S7_HITS="$S7_HITS$h
|
||
"
|
||
done < <(find "$DIR_CORE" -name '*.gd' 2>/dev/null | sort)
|
||
}
|
||
# 符号清单(2026-10-25 对 architecture.md「模块归属」表核录;改符号先改文档再改此处)
|
||
s7_scan B2 game/core/unit_container.gd \
|
||
'spawn|query_alive|rollable_skills|special_skills|check_skill_unlocks|set_pending_action|pending_action|basic_streak|commit_action|tick_cooldowns|apply_damage|heal|default_target|despawn_all'
|
||
s7_scan B3 game/core/status_container.gd \
|
||
'notify_battle_start|apply_effect|query_active|decay_round|notify_battle_end'
|
||
s7_scan B4 game/core/reward_economy.gd \
|
||
'roll_board|query_identity|query_full_snapshot|grant|grant_clear|retire_mapping|polarity_row|phase'
|
||
if [ -n "$S7_HITS" ]; then
|
||
echo "[static_scan] S7 FAIL: B2/B3/B4 API 符号越出 B1 文件集命中 $(printf '%s' "$S7_HITS" | grep -c .) 处(编排唯一性,B1 AC-43)"
|
||
printf '%s' "$S7_HITS"
|
||
fail=1
|
||
else
|
||
echo "[static_scan] S7 ok(扫描面 $DIR_CORE 共 $(scan_files "$DIR_CORE") 个 .gd,B2/B3/B4 API 符号全部收敛于 B1 文件集)"
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
exit $fail
|