并master(闸机提速与防作废批合回)——02分片并行+代码域起手自检+worktree素材同步,与对拍退役批同改闸脚本,冲突就地解

This commit is contained in:
DKChan committed 2026-10-30 02:46:54 +08:00
commit 6b17d8f307
4 files changed
+188 -15

No files matched your search

+77 -2
View File
@@ -4,6 +4,14 @@
# 零命中/空值=用法错 quit(2)。两个来源分开定性——开发期直跑(默认)仅供最窄确认,不产闸回执、
# 不作验收凭据;闸机关联档选跑(--only-source=gates,由 tools/run_all_gates.sh 在 02 闸传入)
# 是收口验收跑批本体,回执 scope=related。无任何选跑参数=全量态,行为与全量口径完全一致。
# 分片态:追加 `-- --shard=k --shard-count=n`(须成对,1≤k≤n,n≥2 才有意义)——把选套集
# (--only 过滤后)切成 n 片互斥子集,本进程只跑第 k 片;闸机据此多进程并行缩短 02 闸墙钟。
# 分片分配走「串行道优先」:开 RNG 事件日志会话的套件(SERIAL_PATTERNS,new_adventure 调用方)
# 全部钉第 1 片——事件日志按种子命名同文件追加(user://logs/rng_log_<seed>.jsonl),跨片并行
# 写同一文件互踩内容,断言日志纯净序的套件(cross_system_flow)必读错行。新增日志会话套件必须
# 同步登记 SERIAL_PATTERNS,漏登记=并行期日志互踩假红。空片合法(--only 小集 × 多片):空片
# 直接 quit(0) 放行,判定职责由其余片承担。各片独立退出码,由闸机聚合;片内三硬闸与
# 快速失败语义不变,逐套件日志名加 _s<k> 段防跨片覆写。
# 协议:套件清单逐条"套件 → 期望退出码";致命类独立进程(expect=1)。
# 执行形态:逐套件起独立 Godot 子进程实跑
#(OS.get_executable_path() 解析本引擎二进制——--script 模式无 autoload/全局标识符,
@@ -30,6 +38,23 @@ const SUITE_TIMEOUT_DEFAULT_SEC := 120
# 快速失败阈值(可调常量,初值 3):连续 N 个套件超时即中止整轮。
const CONSECUTIVE_TIMEOUT_ABORT := 3
# 串行道套件(路径子串匹配):开 RNG 事件日志会话(new_adventure)的调用方——事件日志按种子
# 命名同文件追加(user://logs/rng_log_<seed>.jsonl),跨分片并行写同一文件互踩;断言日志内容
# 的套件(cross_system_flow)必须读到本会话纯净追加序。分片分配时全部钉第 1 片(见头注分片态)。
# 新增日志会话套件时同步登记此处,漏登记=并行期假红。
const SERIAL_PATTERNS: Array[String] = [
"board_generator_test",
"node_map_generator_test",
"minesweeper_rules_test",
"rng_stream_boot_test",
"cross_system_flow_test",
"node_map_determinism_test",
]
# 分片态成员:0=未分片(单进程原路径);>0=第 _shard_k/_shard_n 片(_run_suite 取名用)。
var _shard_k := 0
var _shard_n := 0
func _init() -> void:
# 选跑态解析:--only=<词[,词…]>(经 -- 分隔的 user args)——对套件 res:// 路径做子串匹配;
@@ -46,6 +71,22 @@ func _init() -> void:
only_terms.append(term)
elif a == "--only-source=gates":
only_from_gates = true
elif a.begins_with("--shard="):
var sv := a.trim_prefix("--shard=").strip_edges()
if sv.is_valid_int():
_shard_k = int(sv)
elif a.begins_with("--shard-count="):
var sn := a.trim_prefix("--shard-count=").strip_edges()
if sn.is_valid_int():
_shard_n = int(sn)
if (_shard_k == 0) != (_shard_n == 0):
push_error("[run_all] --shard 与 --shard-count 必须成对出现")
quit(2)
return
if _shard_n < 0 or (_shard_n > 0 and (_shard_k < 1 or _shard_k > _shard_n)):
push_error("[run_all] 分片参数非法(须 1≤k≤n,n≥1):k=%d n=%d" % [_shard_k, _shard_n])
quit(2)
return
if has_only and only_terms.is_empty():
push_error("[run_all] --only= 空值——用法错(选跑禁把零套件当全量)")
quit(2)
@@ -64,6 +105,14 @@ func _init() -> void:
print("[run_all] GATE FILTERED RUN — 闸机关联档选跑 %d/%d 套件(--only=%s)" % [suites.size(), manifest.size(), ",".join(only_terms)])
else:
print("[run_all] FILTERED RUN — 选跑 %d/%d 套件(--only=%s)——仅供开发期最窄确认,不产闸回执、不作验收凭据" % [suites.size(), manifest.size(), ",".join(only_terms)])
var sharded := _shard_n > 1
if sharded:
suites = _shard_suites(suites, _shard_k, _shard_n)
if suites.is_empty():
print("[run_all] SHARD %d/%d — 本片零套件,空片放行(判定职责由其余片承担)" % [_shard_k, _shard_n])
quit(0)
return
print("[run_all] SHARD %d/%d — 本片 %d 套件" % [_shard_k, _shard_n, suites.size()])
print("[run_all] unit/contract suite entry — %d suites" % suites.size())
if DisplayServer.get_name() != "headless":
push_warning("[run_all] 建议以 --headless 运行(D2 冒烟协议)")
@@ -101,7 +150,10 @@ func _init() -> void:
push_error("[run_all] %d 个套件失败" % failures)
quit(1)
return
print("[run_all] all green (%d suites%s)" % [suites.size(), ",filtered" if filtered else ""])
var tag := ",filtered" if filtered else ""
if sharded:
tag += ",shard %d/%d" % [_shard_k, _shard_n]
print("[run_all] all green (%d suites%s)" % [suites.size(), tag])
quit(0)
@@ -203,7 +255,8 @@ func _run_suite(suite: Dictionary, idx: int, filtered: bool = false) -> Dictiona
# 后又没有退出码读取 API,故由 cmd /V:ON 在套件退出后把 !ERRORLEVEL! 回显落盘,
# 值与阻塞式 execute 经 cmd 回传的同一。
var proj := ProjectSettings.globalize_path("res://")
var log := proj + "/tests/.gates/run_all%s_%02d.log" % ["_filter" if filtered else "", idx]
var shard_tag := "" if _shard_n == 0 else "_s%d" % _shard_k
var log := proj + "/tests/.gates/run_all%s%s_%02d.log" % ["_filter" if filtered else "", shard_tag, idx]
var codefile := log + ".code"
if FileAccess.file_exists(codefile):
DirAccess.remove_absolute(codefile) # 陈值防护:上轮临界残件不许冒充本轮退出码
@@ -293,6 +346,28 @@ func _filter_suites(manifest: Array, terms: PackedStringArray) -> Array:
return out
# 分片分配:串行道套件(SERIAL_PATTERNS 命中)全钉第 1 片;其余按片轮转(片内相对序 j%n+1)。
# 分配对每个 k 确定性、互斥且完备——各片并起来=传入全集零重复零遗漏。
func _shard_suites(suites: Array, k: int, n: int) -> Array:
var out: Array = []
var j := 0
for suite in suites:
var path: String = suite.get("path", "")
var is_serial := false
for pat in SERIAL_PATTERNS:
if path.contains(pat):
is_serial = true
break
if is_serial:
if k == 1:
out.append(suite)
else:
if j % n + 1 == k:
out.append(suite)
j += 1
return out
# 引擎二进制必须取 console 版:OS.get_executable_path() 返回的是 GUI 子系统 exe
#(Godot_v4.7.2-stable_win64.exe),它起的子进程 stdout/stderr 句柄无效——输出全丢,
# 脚本错误扫描就成了扫空气。GODOT_BIN 优先,其次按命名推导 *_console.exe。
+57 -5
View File
@@ -300,6 +300,24 @@ if in_domain code; then
fi
fi
# ── 代码域起手自检(闸承载:只省烧闸,不做合回判定——真判定在 00–11 与合回 guard)──
if in_domain code; then
# worktree 素材同步:素材走 gitignore 不入 git,worktree 冷检出缺件=呈现断言红的环境成因;
# 主工作区直跑=脚本内自动无操作。
bash "$REPO/tools/sync_worktree_assets.sh"
# 熵增预检:垃圾目录/越界文件先于长跑批暴露(熵增闸在批末,撞上才红=整轮白烧)。
if ! "$PY" "$REPO/tools/repo_structure_audit.py" >"$STAMP_DIR/entropy_preflight.log" 2>&1; then
echo "[gates] 起手自检红:熵增预检不过——顶层目录越界/根文件异动,先清理再跑闸(日志 $STAMP_DIR/entropy_preflight.log)"
grep -aE "越界|✗|违例" "$STAMP_DIR/entropy_preflight.log" | tail -5 | sed 's/^/ | /'
exit 1
fi
# master 推进预警(worktree 才会触发):master 已越过本分支=合回 guard 第 6 况必拦,
# 先并 master 再出闸,省一轮白烧;本闸回执照出(开发期跑批不受阻)。
if [ -f "$RECEIPT" ] && ! git -C "$REPO" merge-base --is-ancestor master HEAD 2>/dev/null; then
echo "[gates] 警告:master 已推进至本分支之外——本闸回执照出,但合回前须并 master 重出闸。"
fi
fi
echo "闸机跑批 · 域=$DOMAIN · 仓库 $REPO"
echo "起跑 commit: $(git -C "$REPO" rev-parse --short HEAD 2>/dev/null || echo '非git目录')"
echo "python: $PY"
@@ -322,24 +340,58 @@ gate() { # gate <名称> <日志名> <命令...>
return 0
}
gate_unit() { # run_all 是 --script 模式,退出码为唯一判据
gate_unit() { # run_all 是 --script 模式,退出码为唯一判据;分片态=多 Godot 进程各跑一片
local name="02 单元与集成测试"
# 关联档选跑:把套件词列表经 --only= 传给 run_all(闸机来源标记 --only-source=gates,
# 入口据此打关联档横幅、不落「仅供开发期」免责句);全量态不传任何选跑参数。
local -a _user_args=()
if [ "$SCOPE" = related ] && [ -n "$ONLY_TERMS" ]; then
_user_args=(-- "--only=${ONLY_TERMS// /,}" --only-source=gates)
_user_args+=("--only=${ONLY_TERMS// /,}" "--only-source=gates")
fi
local _shards="${GATE_SHARDS:-4}"
echo ""
echo "════════════════════════════════════════════"
echo " $name(--script 模式,退出码为唯一判据)"
if [ "$SCOPE" = related ] && [ -n "$ONLY_TERMS" ]; then
echo " 关联档选跑:--only=$ONLY_TERMS"
fi
if [ "$_shards" -gt 1 ]; then
echo " 分片并行:$_shards 进程(GATE_SHARDS 可调,=1 回落单进程原路径)"
fi
echo "════════════════════════════════════════════"
( cd "$REPO" && "$GODOT" --headless --path "$(cygpath -m "$GAME")" --script res://tests/run_all.gd "${_user_args[@]}" ) \
>"$STAMP_DIR/unit.log" 2>&1
local rc=$?
local rc=0
if [ "$_shards" -le 1 ]; then
local -a _cmd=(--headless --path "$(cygpath -m "$GAME")" --script res://tests/run_all.gd)
[ ${#_user_args[@]} -gt 0 ] && _cmd+=(-- "${_user_args[@]}")
( cd "$REPO" && "$GODOT" "${_cmd[@]}" ) \
>"$STAMP_DIR/unit.log" 2>&1
rc=$?
else
# 分片并行:串行道套件由 run_all 内部分配钉第 1 片(RNG 日志会话跨片互踩,见其头注);
# 各片独立日志,退出码聚合(任一片非 0=闸红);日志按片序合并供错误扫描与指纹取材
#(指纹只取 FAIL 行排序集合、绿闸记 green,片序与合并不改变指纹)。
local -a _pids=()
local k
for k in $(seq 1 "$_shards"); do
( cd "$REPO" && "$GODOT" --headless --path "$(cygpath -m "$GAME")" --script res://tests/run_all.gd \
-- "${_user_args[@]}" "--shard=$k" "--shard-count=$_shards" ) \
>"$STAMP_DIR/unit_shard_$k.log" 2>&1 &
_pids+=($!)
done
local _shard_rc _any=0
for k in $(seq 1 "$_shards"); do
wait "${_pids[$((k - 1))]}"
_shard_rc=$?
echo "[run_all] 分片 $k/$_shards 退出码 $_shard_rc"
[ "$_shard_rc" -ne 0 ] && _any=1
done
[ "$_any" -ne 0 ] && rc=1
: >"$STAMP_DIR/unit.log"
for k in $(seq 1 "$_shards"); do
echo "── unit 分片 $k/$_shards ──" >>"$STAMP_DIR/unit.log"
cat "$STAMP_DIR/unit_shard_$k.log" >>"$STAMP_DIR/unit.log"
done
fi
# 三硬闸之二(脚本错误扫描):子套件的 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
+54
View File
@@ -0,0 +1,54 @@
#!/usr/bin/env bash
# worktree 素材同步:主工作区 → 本 worktree 增量拷贝 gitignore 面(game/assets/art/ 终稿与 .import 边车)。
# 素材不入 git,worktree 冷检出与主区新收素材之间必然缺件——呈现/样张断言红的环境成因。
# 只在 worktree 生效(主工作区直跑=无操作);有新拷件或缺 .godot 时跑一次编辑器导入刷新
#(headless --script 模式不触发 import,素材在盘而 .ctex 缺位照样加载失败)。
# 用法:tools/sync_worktree_assets.sh(闸机 code 域起手自动调用;可单独手跑)
# 纪律:拷贝一律相对路径定位(在源根 cd 后按相对路径落盘)——绝对路径 --parents 会在
# 目标仓顶层误产盘符目录树(杂散目录即此误产,熵增闸判红)。
set -u
REPO="$(cd "$(dirname "$0")/.." && pwd)"
GITDIR="$(git -C "$REPO" rev-parse --git-dir 2>/dev/null)"
COMMON="$(git -C "$REPO" rev-parse --git-common-dir 2>/dev/null)"
if [ -z "$GITDIR" ] || [ -z "$COMMON" ] || [ "$GITDIR" = "$COMMON" ]; then
echo "[assets] 主工作区:无同步面"
exit 0
fi
MAIN="$(git -C "$REPO" worktree list --porcelain 2>/dev/null | sed -n 's/^worktree //p' | head -1)"
if [ -z "$MAIN" ]; then
echo "[assets] 取不到主工作区路径(git worktree list 为空),跳过同步"
exit 0
fi
MAIN="$(cd "$MAIN" && pwd)"
[ "$MAIN" = "$REPO" ] && exit 0
_copied=0
if [ -d "$MAIN/game/assets/art" ]; then
_new="$(comm -13 <(cd "$REPO/game/assets/art" 2>/dev/null && find . -type f | sort) \
<(cd "$MAIN/game/assets/art" && find . -type f | sort) | wc -l)"
if [ "$_new" -gt 0 ]; then
mkdir -p "$REPO/game/assets/art"
cp -ru "$MAIN/game/assets/art/." "$REPO/game/assets/art/"
_copied=$((_copied + _new))
fi
fi
_imports="$(comm -13 <(cd "$REPO/game" 2>/dev/null && find . -name '*.import' -type f | sort) \
<(cd "$MAIN/game" && find . -name '*.import' -type f | sort) | wc -l)"
if [ "$_imports" -gt 0 ]; then
(cd "$MAIN/game" && find . -name '*.import' -type f -print0) | while IFS= read -r -d '' f; do
if [ ! -f "$REPO/game/$f" ]; then
mkdir -p "$REPO/game/$(dirname "$f")"
cp "$MAIN/game/$f" "$REPO/game/$f"
fi
done
_copied=$((_copied + _imports))
fi
if [ "$_copied" -gt 0 ] || [ ! -d "$REPO/game/.godot" ]; then
echo "[assets] worktree 素材同步:补拷 $_copied 件(含 .import 边车),刷新导入缓存"
"${GODOT_BIN:-/d/code/godot/Godot_v4.7.2-stable_win64_console.exe}" \
--headless --editor --quit --path "$REPO/game" >/dev/null 2>&1
echo "[assets] 导入刷新完成"
else
echo "[assets] worktree 素材同步:无缺件"
fi
exit 0
-8
View File
@@ -2,14 +2,6 @@
本清单是流水线的唯一登记处:要做的事先在此登记一条,随流水线段推进改状态,做完整条删除;各系统文档的「待办」节只留一行指路存根,不再自建条目。条目格式(标题=登记元数据含可选优先级,正文=实况/缺口/改动面/状态四属性)与状态机以 [doc-conventions §12.1](design/engineering/doc-conventions.md) 为权威。
#### 闸机提速与防作废批——02 分片并行+代码域起手自检+worktree 素材同步(2026-10-30 登记,来源=会话提及)
**实况**:迭代 42 合回六轮被拦复盘:单轮代码域闸约 24 分钟(02 串行 66 套件约 5.5 分钟+探针渲染约 3.5 分钟+对拍实跑约 14 分钟),六轮累计约 2 小时;被拦主因=master 中途被另一会话推进(回执作废整轮重跑)、worktree 缺 gitignore 素材(迭代 43 新套件断言的 PNG 不入 git)、闸进程被会话取消连带杀掉、操作失误产杂散目录到批末熵增闸才红。证据=`tools/run_all_gates.sh:325`(gate_unit 单 Godot 进程串行编排)、`game/tests/run_all.gd:206`(逐套件独立子进程实跑、编排层串行为唯一瓶颈)、`tools/run_all_gates.sh:470`(回执指纹绿闸记 green、红闸取 FAIL 行排序集合——输出顺序不影响指纹,并行化免记账换代)、`game/core/rng_service.gd:134`(事件日志按种子命名同文件追加——开日志会话的套件跨分片并行互踩,须钉同一分片串行)。本批内容:B=`run_all_gates.sh` code 域起手自检(熵增预检红灯即止、master 推进黄牌、worktree 素材自动同步 `tools/sync_worktree_assets.sh`)+iter 技能合回段补「并 master → 自检 → 出闸 → 立即合并」次序;A=`run_all.gd` 加 `--shard/--shard-count` 分片(串行道套件钉第 1 片)+`gate_unit` 多进程并行(`GATE_SHARDS` 可调,=1 回落单进程原路径)。C 批(探针/渲染三段并行)本轮缓做不立项。
**改动面**:工具代码
**状态**:待执行
#### 架构审查优化讨论——全库架构审计读数的逐条裁决与整改分批(2026-10-29 登记,来源=审计发现)
**实况**:2026-10-29 全库架构静态审计完成,读数见 reports/审计-全库架构审查-2026-10-29.md(七节:总评/健康面/P0/P1/P2/待裁点/取证口径;零测试执行零引擎运行,全部结论带 文件:行号)。健康面已逐条核干净:Core 零 UI 八项判据全零命中、core 内 RNG 单点 100% 成立、字符串式信号连接全库 0 处、core 层零超长函数、19 表 ↔ 19 schema 一一对应、无循环 preload。问题集中在三条「写了没闸守」的纪律被实际破坏:①数值单一事实源被 18 处硬编码击穿,最危险两处是 `presentation/main.gd:787` 与 `:790` 把 `rap_refresh_cost=8` 与 `refresh_cap_per_round=2` 写死进提示文案(紧邻 783 行是正确读表的),表改文案不改、玩家直接受错误数字;另有 4 组代码自标「机制结构常量/产出约束」的字面量(`node_map_generator.gd:12` `MAX_WIDTH`、`action_economy.gd:14` `CHARGE_LAYER_CAP`、`board_data.gd:9` `SPAWN_AREA_CELLS`、`node_map_generator.gd:83-94` 域字面量)是否入表待裁。②`game/sim/` 与 `game/core/` 四组平行实现——扫雷推理引擎双份(`board_generator.gd` `measure_inferability` 族 vs `auto_player.gd` `_deduce` 族,输入集口径不同:前者含 safe_set oracle、后者 no-peek)、伤害掷点期望重算(`damage_probe.gd:196-201`)、档位排序与驱动循环双份(`level_autoplay.gd` vs `treasure_item_table.gd:44` 与 `battle_driver.gd:17`)、FakeDB 伪造表喂生成器——直接威胁「模拟读数 = 游戏真实数值」这一数值验证前提。③RNG 单点纪律在 core 外失守(sim 4 处/probe 1 处/tests 3 处自建 `RandomNumberGenerator`,均显式定种、确定性尚存),而 `static_scan.sh` 的 S1/S2 扫描面只含 `DIR_CORE` + `DIR_PRES`。守卫面另有三缺口:S8 是 9 文件显式清单(新增结算文件忘登记=静默不扫,缺位自检只防删改不防增)、S9 类型闸与 S13 质量闸扫描面止步于 core+data+presentation(sim/probe/tests 三层除 S11/S12 外零闸)、S13 三处豁免常态化到最大户文件(`battle_orchestrator`/`main.gd`/`tune_sim_controller`);另有四个 core 文件零测试点名(`attune_loadout`/`battle_fx_host`/`meta_state_store`/`slot_store`)。P2 级:`core/settlement/` 五段类直读写编排器私有成员 20+ 处(`oc._damage.*`/`oc._enemy_system.*`,GDScript 无编译期检查、改名静默断裂);架构文档三处漂移(`architecture.md:110` 登记的 `core/hero_ledger.gd` 盘上不存在且 `hero_ledger_test.gd:171` 显式断言它必须不存在、`:135` 行数过期、EC8 热重载门后 `ConfigDb.reload()` 零调用方且走 `load()` 拿不到磁盘新值);class_name 级双向依赖(`core/config_db` → `data/schemas/board_gen_table` → `core/board_data.SPAWN_AREA_CELLS`)与 `NODE_TYPES` 词表双份;`MetaProgression` 是唯一用 `get_node("/root/...")` 取依赖、唯一跨 Autoload 直调的单例。