Merge branch 'master' into iter40

This commit is contained in:
DKChan committed 2026-10-27 08:00:37 +08:00
commit 3a20a40891
12 files changed
+127 -38

No files matched your search

+3
View File
@@ -7,6 +7,9 @@
# 导入元数据 # 导入元数据
*.import *.import
# 代码面 worktree 的 Godot user 目录隔离件(tools/setup_worktree_userdir.sh 生成;主区不生成)
game/override.cfg
# 导出构建产物 # 导出构建产物
builds/ builds/
exports/ exports/
+2 -2
View File
@@ -9,7 +9,7 @@
改动按文件性质分两车道(2026-10-26 用户裁,取代 2026-09-19 立的「一切改动走 worktree」铁律;判据与 §3 第 8 条改动域划分同一): 改动按文件性质分两车道(2026-10-26 用户裁,取代 2026-09-19 立的「一切改动走 worktree」铁律;判据与 §3 第 8 条改动域划分同一):
1. **worktree 批车道(唯一强制面=代码面)**:一切触及代码面(`game/` 下非 `.md` 的行为载体:代码、测试脚本、探针、场景、数据表、素材)的改动——迭代实现批与触及代码面的工程改动(如纯测试维护)——先建独立 worktree:`git worktree add ../g-plan2-<任务名> -b <任务分支>`;所有改动与验证都在 worktree 内进行(未跟踪文件不会出现在新 worktree,需手工搬运)。迭代批开工细则见 §3 与 iter 技能 1. **worktree 批车道(唯一强制面=代码面)**:一切触及代码面(`game/` 下非 `.md` 的行为载体:代码、测试脚本、探针、场景、数据表、素材)的改动——迭代实现批与触及代码面的工程改动(如纯测试维护)——先建独立 worktree:`git worktree add ../g-plan2-<任务名> -b <任务分支>`;所有改动与验证都在 worktree 内进行(未跟踪文件不会出现在新 worktree,需手工搬运);建区后立刻跑 `tools/setup_worktree_userdir.sh`——给该区独立 Godot user 目录(测试夹具、试玩日志、截图产物不与其他区共用;主区不生成,真实档保持原位)。迭代批开工细则见 §3 与 iter 技能
2. **worktree 批收尾**:批次收口顺序固定为**冻结 → 件核 → 并 master → 跑一次域闸 → 合回**——代码与断言改完即冻结,全仓件核 grep 查残留;master 已被别的批次推进时先 `git merge master` 解冲突(必须在跑闸之前,闸跑完再并=那次闸白跑),然后按本批改动域跑**一次** `tools/run_all_gates.sh`(见 §3.9),全绿后在主工作区 `git merge --no-ff <任务分支>`,再 `git worktree remove`+删分支。闸后代码面不得再改一个字节(回执作废须重跑),只允许追加 `reports/`、`design/` 等文档件提交。游戏类测试(冒烟/单测/MC/探针)批内默认零执行(两入口铁规,见 §4),用户点名或当场问才跑 2. **worktree 批收尾**:批次收口顺序固定为**冻结 → 件核 → 并 master → 跑一次域闸 → 合回**——代码与断言改完即冻结,全仓件核 grep 查残留;master 已被别的批次推进时先 `git merge master` 解冲突(必须在跑闸之前,闸跑完再并=那次闸白跑),然后按本批改动域跑**一次** `tools/run_all_gates.sh`(见 §3.9),全绿后在主工作区 `git merge --no-ff <任务分支>`,再 `git worktree remove`+删分支。闸后代码面不得再改一个字节(回执作废须重跑),只允许追加 `reports/`、`design/` 等文档件提交。游戏类测试(冒烟/单测/MC/探针)批内默认零执行(两入口铁规,见 §4),用户点名或当场问才跑
3. **直改车道(代码面之外的一切面)**:`design/`、`reports/`、`tools/`、根规则与清单文件、`game/` 内 `.md`、`.zcode/`、`.qoder/`——主工作区直接改、直接提交,无 worktree、无冻结/件核/合并仪式。验收=commit 点 git pre-commit 钩子(`tools/hooks/pre-commit`,经 `core.hooksPath` 挂载,在位性由 run_all_gates.sh 跑批自检)现场跑 `tools/run_all_gates.sh --docs`(秒级文本扫描):绿即落地,红即拦下,修红后重新 commit 钩子自动重跑;跑完把闸输出呈报对话即完成义务。预期大体量改动(跨文档结构重构等)时由用户开工点名自愿开 worktree,非规则强制 3. **直改车道(代码面之外的一切面)**:`design/`、`reports/`、`tools/`、根规则与清单文件、`game/` 内 `.md`、`.zcode/`、`.qoder/`——主工作区直接改、直接提交,无 worktree、无冻结/件核/合并仪式。验收=commit 点 git pre-commit 钩子(`tools/hooks/pre-commit`,经 `core.hooksPath` 挂载,在位性由 run_all_gates.sh 跑批自检)现场跑 `tools/run_all_gates.sh --docs`(秒级文本扫描):绿即落地,红即拦下,修红后重新 commit 钩子自动重跑;跑完把闸输出呈报对话即完成义务。预期大体量改动(跨文档结构重构等)时由用户开工点名自愿开 worktree,非规则强制
4. **执法分层(全机械,零自觉依赖)**:①git pre-commit 钩子——直改车道的闸机检,并对主工作区直提代码面的提交一律拦(代码面唯一合法路径=worktree 批合回);任务 worktree 内的代码面提交跳过(归批末闸+合回闸管,不打扰迭代批中间提交)。②`.zcode/hooks/worktree-guard.sh`(PreToolUse,本机件不入 git)——agent 侧:主工作区代码面的 Edit/Write 与 staged 含代码面的 commit 拦截、`git commit --no-verify` 拦截(该旗标仅属用户)、合回闸回执六况校验(§3.9)。git 层钩子对一切提交者生效(含用户手工提交,紧急出口 `--no-verify`);ZCode 层只约束 agent 4. **执法分层(全机械,零自觉依赖)**:①git pre-commit 钩子——直改车道的闸机检,并对主工作区直提代码面的提交一律拦(代码面唯一合法路径=worktree 批合回);任务 worktree 内的代码面提交跳过(归批末闸+合回闸管,不打扰迭代批中间提交)。②`.zcode/hooks/worktree-guard.sh`(PreToolUse,本机件不入 git)——agent 侧:主工作区代码面的 Edit/Write 与 staged 含代码面的 commit 拦截、`git commit --no-verify` 拦截(该旗标仅属用户)、合回闸回执六况校验(§3.9)。git 层钩子对一切提交者生效(含用户手工提交,紧急出口 `--no-verify`);ZCode 层只约束 agent
@@ -40,7 +40,7 @@
3. **用户发开工令才动手**——需求卡片经用户批准(U0)后方开工 3. **用户发开工令才动手**——需求卡片经用户批准(U0)后方开工
**环境与基线** **环境与基线**
4. **代码面批独立 worktree+拷导入缓存**——触及代码面的批次按 §1 建 worktree;先拷主区 `.godot/` 目录与字体 `.import` 边车再跑测试,否则全量重导入资源近似卡死。检出工作区零测试,直接开工(2026-10-22 频率规则);直改车道(非代码面)无此项 4. **代码面批独立 worktree+拷导入缓存**——触及代码面的批次按 §1 建 worktree;先拷主区 `.godot/` 目录与字体 `.import` 边车再跑测试,否则全量重导入资源近似卡死。检出工作区零测试,直接开工(2026-10-22 频率规则);闸壳每次跑批先自检/自愈本区 user 目录隔离(§1 第 1 条),建不起来即拒跑;直改车道(非代码面)无此项
5. **涉数值改动先采改前读数**——与改后同随机种子配对比较并写明逐项比对口径;无数值改动禁跑模拟 5. **涉数值改动先采改前读数**——与改后同随机种子配对比较并写明逐项比对口径;无数值改动禁跑模拟
**实施** **实施**
+2 -2
View File
@@ -1,5 +1,5 @@
{ {
"note": "存量基线·首次自动生成;闸况变化时本脚本会判红,需人工确认后重生成;2026-10-17 人工确认重生成 06 条目:af94ce97 把 gdd_lint 记为存量红(01 篇 R5),该红已由 fcb8c3f8 补流程图消除,实测 --docs 域 06 exit=0,故基线改回绿。2026-10-18 新增闸件 08c 流水线闸(tools/pipeline_gate.py 注册上电),手工加入绿记录(roadmap 压缩批实测 --docs 域 exit=0),其余条目原样保留。2026-10-22 测试术语与层级重构批 A 改键:01 冒烟全量→01 系统测试(同一断言面 smoke_all.gd,运行器换 tools/run_system_test.py,过渡期判定口径不变)、02 单元套件→02 单元与集成测试(同一 run_all.gd 入口,名从实)、新增 00 冒烟测试(tools/run_smoke.py+game/tests/smoke_boot.tscn,重写后单跑实测 17 项 0 失败 PASS);09 静态扫描基线 exit=1(fp=4354b2aa72a7)系滞后红,本批单跑实测 exit=0(与 2026-10-22 16:20 回执一致),基线改回绿;其余条目原样保留。;2026-10-23 主会话记 07 doc_link 为存量红(用户令忽略:锻造工坊.md 死链的修复在另一会话未合入,合回闸豁免;外部修复合入后应重生成基线归绿);2026-10-26 用户特批带红合并(迭代 38 收口):09 静态扫描记存量红——gdstyle 恶化 2 条(battle_orchestrator.gd max-file-length 1314>1302=卡④头注必载内容、level_map_screen.gd max-class-variables 22>21=卡①缓存成员,均为卡片义务的记账漂移非缺陷,压缩还债经计算不可行),用户裁「不管引入的恶化直接提交、特许免测试重跑」;后续批次若触及两文件可顺带还债或经批准重生成基线归绿", "note": "存量基线·首次自动生成;闸况变化时本脚本会判红,需人工确认后重生成;2026-10-17 人工确认重生成 06 条目:af94ce97 把 gdd_lint 记为存量红(01 篇 R5),该红已由 fcb8c3f8 补流程图消除,实测 --docs 域 06 exit=0,故基线改回绿。2026-10-18 新增闸件 08c 流水线闸(tools/pipeline_gate.py 注册上电),手工加入绿记录(roadmap 压缩批实测 --docs 域 exit=0),其余条目原样保留。2026-10-22 测试术语与层级重构批 A 改键:01 冒烟全量→01 系统测试(同一断言面 smoke_all.gd,运行器换 tools/run_system_test.py,过渡期判定口径不变)、02 单元套件→02 单元与集成测试(同一 run_all.gd 入口,名从实)、新增 00 冒烟测试(tools/run_smoke.py+game/tests/smoke_boot.tscn,重写后单跑实测 17 项 0 失败 PASS);09 静态扫描基线 exit=1(fp=4354b2aa72a7)系滞后红,本批单跑实测 exit=0(与 2026-10-22 16:20 回执一致),基线改回绿;其余条目原样保留。;2026-10-23 主会话记 07 doc_link 为存量红(用户令忽略:锻造工坊.md 死链的修复在另一会话未合入,合回闸豁免;外部修复合入后应重生成基线归绿);2026-10-26 用户特批带红合并(迭代 38 收口):09 静态扫描记存量红——gdstyle 恶化 2 条(battle_orchestrator.gd max-file-length 1314>1302=卡④头注必载内容、level_map_screen.gd max-class-variables 22>21=卡①缓存成员,均为卡片义务的记账漂移非缺陷,压缩还债经计算不可行),用户裁「不管引入的恶化直接提交、特许免测试重跑」;后续批次若触及两文件可顺带还债或经批准重生成基线归绿;2026-10-27 用户裁「标准是尺、不以现状为尺」:09 存量红照实记(exit=1+现行红项指纹,两条恶化各自点名可核),gdstyle 基线登记值回退 1302/21(波 0 的 18 条陈旧键清理保留),gdstyle_gate 逐条违规行加 FAIL 前缀并加「refresh 禁抬高登记值」守卫——存量红期间任何新增违规或进一步恶化都会改指纹被拦;阈值口径不变=gdstyle 默认值(文件 1000 行/类变量 15 等),还债归迭代 40 卡①(battle_orchestrator 1314→≤1000)与迭代 41 卡③(level_map_screen 22→≤15),还清后重新生成基线归绿",
"gates": { "gates": {
"00 冒烟测试": { "00 冒烟测试": {
"exit": "0", "exit": "0",
@@ -47,7 +47,7 @@
}, },
"09 静态扫描": { "09 静态扫描": {
"exit": "1", "exit": "1",
"fp": "user-approved-iter38" "fp": "91d45583cf6d"
}, },
"10 熵增闸": { "10 熵增闸": {
"exit": "0", "exit": "0",
+4 -2
View File
@@ -291,8 +291,10 @@ fi
# probe/sim/tests 测量面直构惯例不在面)。存量基线 tools/gdstyle_baseline.txt 用稳定身份键(函数级带数 # probe/sim/tests 测量面直构惯例不在面)。存量基线 tools/gdstyle_baseline.txt 用稳定身份键(函数级带数
# 条目=文件|规则|函数名,类级/文件级带数条目=文件|规则,均带登记数值;无数字条目=文件|规则|消息全文 # 条目=文件|规则|函数名,类级/文件级带数条目=文件|规则,均带登记数值;无数字条目=文件|规则|消息全文
# 裸键)——消息里的行数/分支数等计数漂移不假红;判定=基线外新违规红(NEW)+同键指标较登记值恶化红 # 裸键)——消息里的行数/分支数等计数漂移不假红;判定=基线外新违规红(NEW)+同键指标较登记值恶化红
# (WORSENED),改小/持平不红。基线重生成=人工专用(python tools/gdstyle_gate.py --refresh,附对账; # (WORSENED),改小/持平不红;逐条违规行带 FAIL 前缀,使存量红期间的红项清单(哪几条、恶化到多少)
# 键数骤降逾 10% 须 --force),禁清基线换绿。二进制本机固定路径 # 进 gate 09 指纹,新增或进一步恶化不会被「同款存量红」放过。阈值口径=gdstyle 默认值;基线只是欠账
# 簿,登记值只准随还债下降,禁抬高到现状。基线重生成=人工专用(python tools/gdstyle_gate.py
# --refresh,附对账;键数骤降逾 10% 或登记值被抬高均拒写,须 --force),禁清基线换绿。二进制本机固定路径
# D:/code/gdstyle/gdstyle.exe(不入库),GDSTYLE_BIN 环境变量覆盖;版本与钉定版不符即红(升级须重测)。 # D:/code/gdstyle/gdstyle.exe(不入库),GDSTYLE_BIN 环境变量覆盖;版本与钉定版不符即红(升级须重测)。
if python tools/gdstyle_gate.py; then if python tools/gdstyle_gate.py; then
echo "[static_scan] S13 ok" echo "[static_scan] S13 ok"
+1
View File
@@ -466,6 +466,7 @@ func _smoke_lifecycle_matrix() -> void:
guard += 1 guard += 1
check(main.run_scope.settlement == null or not main.run_scope.settlement.is_battle_live(), check(main.run_scope.settlement == null or not main.run_scope.settlement.is_battle_live(),
"V3d 活战斗驱动完毕(战斗已收场——%d 轮,finished=%s)〔装配生命批 V3〕" % [guard, str(finished.done)]) "V3d 活战斗驱动完毕(战斗已收场——%d 轮,finished=%s)〔装配生命批 V3〕" % [guard, str(finished.done)])
await process_frame
main._flush_row_queue() main._flush_row_queue()
for i in range(12): for i in range(12):
if main._end_overlay.is_open(): if main._end_overlay.is_open():
+4 -2
View File
@@ -30,6 +30,9 @@ import glob
import json import json
import os import os
import sys import sys
from pathlib import Path
from godot_user_dir import resolve_user_dir
# ---- 表值区(溯源见文件头) ---- # ---- 表值区(溯源见文件头) ----
POOL_CLAMP = (-100, 100) POOL_CLAMP = (-100, 100)
@@ -613,8 +616,7 @@ def print_report(label, per_battle, t, exact_mode):
def main(): def main():
log_dir = os.path.join(os.environ.get("APPDATA", ""), "Godot", "app_userdata", log_dir = str(resolve_user_dir(Path(__file__).resolve().parent.parent) / "logs")
"DungeonSweeperVerticalSlice", "logs")
path = None path = None
args = sys.argv[1:] args = sys.argv[1:]
if args and not args[0].startswith("-"): if args and not args[0].startswith("-"):
+3 -19
View File
@@ -3,19 +3,13 @@
# (class variables/public methods/file length)=文件|规则;无数字条目=文件|规则|消息全文(裸键)。 # (class variables/public methods/file length)=文件|规则;无数字条目=文件|规则|消息全文(裸键)。
# 带数条目行格式「键 = 数值」,数值=消息剥掉 (max N) 段后首个整数;无数字条目裸键。 # 带数条目行格式「键 = 数值」,数值=消息剥掉 (max N) 段后首个整数;无数字条目裸键。
# 判定=键不在基线红(NEW)、同键数值较登记值恶化红(WORSENED)、改小/持平不红。 # 判定=键不在基线红(NEW)、同键数值较登记值恶化红(WORSENED)、改小/持平不红。
# 阈值口径=gdstyle 默认值(tools/gdstyle.toml 不设数值阈值);本文件只是欠账簿,登记值只准随
# 还债下降,禁抬高到现状(抬高=把既有代码当尺子,默认恶化)。
# 重生成=人工专用:python tools/gdstyle_gate.py --refresh(禁清基线换绿;改规则口径/工具升级须随批 # 重生成=人工专用:python tools/gdstyle_gate.py --refresh(禁清基线换绿;改规则口径/工具升级须随批
# 重生成;新键数较旧基线少逾 10% 须加 --force 显式确认)。 # 重生成;新键数较旧基线少逾 10% 或登记值被抬高均拒写,须加 --force 显式确认)。
game/core/action_economy.gd|quality/max-class-variables = 18 game/core/action_economy.gd|quality/max-class-variables = 18
game/core/adventure_runner.gd|quality/max-branches|_apply_curio_pick = 10 game/core/adventure_runner.gd|quality/max-branches|_apply_curio_pick = 10
game/core/adventure_runner.gd|quality/max-branches|_draw_curio_candidates = 11
game/core/adventure_runner.gd|quality/max-branches|resume_adventure = 11
game/core/adventure_runner.gd|quality/max-class-variables = 42 game/core/adventure_runner.gd|quality/max-class-variables = 42
game/core/adventure_runner.gd|quality/max-file-length = 1051
game/core/adventure_runner.gd|quality/max-function-length|_draw_curio_candidates = 85
game/core/adventure_runner.gd|quality/max-function-length|_start_current_level = 53
game/core/adventure_runner.gd|quality/max-function-length|resume_adventure = 108
game/core/adventure_runner.gd|quality/max-local-variables|_draw_curio_candidates = 26
game/core/adventure_runner.gd|quality/max-local-variables|resume_adventure = 13
game/core/adventure_runner.gd|quality/max-public-methods = 23 game/core/adventure_runner.gd|quality/max-public-methods = 23
game/core/adventure_runner.gd|quality/max-returns|_apply_curio_pick = 9 game/core/adventure_runner.gd|quality/max-returns|_apply_curio_pick = 9
game/core/battle_orchestrator.gd|quality/max-branches|begin_battle = 17 game/core/battle_orchestrator.gd|quality/max-branches|begin_battle = 17
@@ -141,9 +135,6 @@ game/data/schemas/reward_table.gd|quality/max-function-length|validate = 99
game/data/schemas/reward_table.gd|quality/max-local-variables|validate = 20 game/data/schemas/reward_table.gd|quality/max-local-variables|validate = 20
game/data/schemas/settlement_table.gd|quality/max-branches|validate = 16 game/data/schemas/settlement_table.gd|quality/max-branches|validate = 16
game/data/schemas/settlement_table.gd|quality/max-class-variables = 18 game/data/schemas/settlement_table.gd|quality/max-class-variables = 18
game/data/schemas/shop_table.gd|quality/max-branches|validate = 22
game/data/schemas/shop_table.gd|quality/max-function-length|validate = 84
game/data/schemas/shop_table.gd|quality/max-local-variables|validate = 20
game/data/schemas/skill_table.gd|quality/max-branches|validate = 29 game/data/schemas/skill_table.gd|quality/max-branches|validate = 29
game/data/schemas/skill_table.gd|quality/max-function-length|validate = 80 game/data/schemas/skill_table.gd|quality/max-function-length|validate = 80
game/data/schemas/status_effect_table.gd|quality/max-branches|validate = 12 game/data/schemas/status_effect_table.gd|quality/max-branches|validate = 12
@@ -162,8 +153,6 @@ game/presentation/hud_presenter.gd|quality/max-parameters|_refresh_out_of_battle
game/presentation/hud_presenter.gd|quality/max-parameters|refresh = 8 game/presentation/hud_presenter.gd|quality/max-parameters|refresh = 8
game/presentation/hud_presenter.gd|quality/max-parameters|tune_sim_report = 10 game/presentation/hud_presenter.gd|quality/max-parameters|tune_sim_report = 10
game/presentation/main.gd|quality/max-class-variables = 49 game/presentation/main.gd|quality/max-class-variables = 49
game/presentation/main.gd|quality/max-file-length = 1507
game/presentation/main.gd|quality/max-function-length|_build_ui = 189
game/presentation/playtest_logger.gd|quality/no-debug-print|debug 'print()' call found game/presentation/playtest_logger.gd|quality/no-debug-print|debug 'print()' call found
game/presentation/treasure_interact_controller.gd|quality/max-local-variables|inventory_projection = 11 game/presentation/treasure_interact_controller.gd|quality/max-local-variables|inventory_projection = 11
game/presentation/treasure_interact_controller.gd|quality/max-parameters|_init = 7 game/presentation/treasure_interact_controller.gd|quality/max-parameters|_init = 7
@@ -192,15 +181,10 @@ game/presentation/views/board_cell_unit.gd|quality/max-local-variables|_draw = 1
game/presentation/views/board_fx_view.gd|quality/allocation-in-loop|object allocation '.new()' inside a loop game/presentation/views/board_fx_view.gd|quality/allocation-in-loop|object allocation '.new()' inside a loop
game/presentation/views/board_view.gd|quality/allocation-in-loop|object allocation '.new()' inside a loop game/presentation/views/board_view.gd|quality/allocation-in-loop|object allocation '.new()' inside a loop
game/presentation/views/board_view.gd|quality/max-branches|_paint_cell = 24 game/presentation/views/board_view.gd|quality/max-branches|_paint_cell = 24
game/presentation/views/board_view.gd|quality/max-branches|_wall_tile_for = 14
game/presentation/views/board_view.gd|quality/max-class-variables = 45 game/presentation/views/board_view.gd|quality/max-class-variables = 45
game/presentation/views/board_view.gd|quality/max-file-length = 1220
game/presentation/views/board_view.gd|quality/max-function-length|_paint_cell = 139 game/presentation/views/board_view.gd|quality/max-function-length|_paint_cell = 139
game/presentation/views/board_view.gd|quality/max-local-variables|_digit_ink_mode = 12
game/presentation/views/board_view.gd|quality/max-local-variables|_paint_cell = 22 game/presentation/views/board_view.gd|quality/max-local-variables|_paint_cell = 22
game/presentation/views/board_view.gd|quality/max-public-methods = 27 game/presentation/views/board_view.gd|quality/max-public-methods = 27
game/presentation/views/board_view.gd|quality/max-returns|_wall_center_tile = 7
game/presentation/views/board_view.gd|quality/max-returns|_wall_tile_for = 15
game/presentation/views/board_zone_view.gd|quality/max-function-length|_init = 59 game/presentation/views/board_zone_view.gd|quality/max-function-length|_init = 59
game/presentation/views/camp_overlay_view.gd|quality/allocation-in-loop|object allocation '.new()' inside a loop game/presentation/views/camp_overlay_view.gd|quality/allocation-in-loop|object allocation '.new()' inside a loop
game/presentation/views/camp_overlay_view.gd|quality/max-class-variables = 19 game/presentation/views/camp_overlay_view.gd|quality/max-class-variables = 19
+26 -9
View File
@@ -16,19 +16,25 @@ quality/*+syntax/reserved-identifier,format/naming/order 全关)。扫描
带数条目行格式「键 = 数值」,数值=消息剥掉 (max N) 段后首个整数;无数字条目裸键无数值。 带数条目行格式「键 = 数值」,数值=消息剥掉 (max N) 段后首个整数;无数字条目裸键无数值。
判定:①键不在基线=新违规(NEW)红;②键在基线且双方有数值、当前数值 > 基线登记值= 判定:①键不在基线=新违规(NEW)红;②键在基线且双方有数值、当前数值 > 基线登记值=
恶化(WORSENED)红——独立于差集,防稳定键吞掉 104→300 行式恶化;③其余绿(改小/持平不红)。 恶化(WORSENED)红——独立于差集,防稳定键吞掉 104→300 行式恶化;③其余绿(改小/持平不红)。
逐条违规行带 FAIL 前缀——gate 09 的存量红指纹只取含 FAIL 的行,带前缀才能让「红项清单」
(哪几条、恶化到多少)进指纹:否则存量红期间的任何新增/进一步恶化都算同款红被放行。
基线重生成=人工专用(--refresh,附新旧对账与骤降守卫),禁在闸内自动重生成(防清基线换绿); 阈值口径=gdstyle 默认值(tools/gdstyle.toml 不设数值阈值);基线只是欠账簿,登记值只准因
新键数较旧基线少逾 10% 时拒绝写入,须加 --force 显式确认(防扫面断裂静默缩基线换绿), 还债下降,禁抬高到现状(抬高=把既有代码当尺子,默认恶化)。
--force 只在 refresh 路径有效。
基线重生成=人工专用(--refresh,附新旧对账、骤降守卫与抬升守卫),禁在闸内自动重生成
(防清基线换绿);新键数较旧基线少逾 10% 拒绝写入(防扫面断裂静默缩基线换绿);任一键登记
值被抬高同样拒绝(防以现状为尺吞掉恶化)。两条守卫都须 --force 显式确认,--force 只在
refresh 路径有效。
二进制:本机固定路径 D:/code/gdstyle/gdstyle.exe(不入库不入包),GDSTYLE_BIN 环境变量覆盖; 二进制:本机固定路径 D:/code/gdstyle/gdstyle.exe(不入库不入包),GDSTYLE_BIN 环境变量覆盖;
版本与钉定版不符即红——升级工具须重测语法兼容性并人工重生成基线。 版本与钉定版不符即红——升级工具须重测语法兼容性并人工重生成基线。
退出码:0=无基线外新违规且无恶化;1=有新违规/有恶化/二进制缺失或版本不符/扫描面为空/ 退出码:0=无基线外新违规且无恶化;1=有新违规/有恶化/二进制缺失或版本不符/扫描面为空/
输出不可解析/refresh 骤降拒写;2=用法错。 输出不可解析/refresh 骤降或抬升拒写;2=用法错。
用法:python tools/gdstyle_gate.py # 闸判定 用法:python tools/gdstyle_gate.py # 闸判定
python tools/gdstyle_gate.py --refresh # 人工重生成基线(覆盖旧文件,附对账) python tools/gdstyle_gate.py --refresh # 人工重生成基线(覆盖旧文件,附对账)
python tools/gdstyle_gate.py --refresh --force # 骤降守卫触发时显式确认写入 python tools/gdstyle_gate.py --refresh --force # 骤降/抬升守卫触发时显式确认写入
""" """
import json import json
import os import os
@@ -171,6 +177,17 @@ def run_gate() -> int:
if len(current) < len(old) * SHRINK_GUARD and not force: if len(current) < len(old) * SHRINK_GUARD and not force:
return fail("新键数 %d 较旧基线 %d 少逾 10%%——疑扫面断裂静默缩基线换绿,拒绝写入;" return fail("新键数 %d 较旧基线 %d 少逾 10%%——疑扫面断裂静默缩基线换绿,拒绝写入;"
"确认缩基线正当(如删规则/缩扫描面)须加 --force 显式确认" % (len(current), len(old))) "确认缩基线正当(如删规则/缩扫描面)须加 --force 显式确认" % (len(current), len(old)))
raised = sorted(
(k, old[k], current[k]) for k in current
if k in old and old[k] is not None and current[k] is not None and current[k] > old[k]
)
if raised and not force:
for k, was, now in raised[:MAX_REPORT]:
print(" FAIL 抬高 %s = %d(登记 %d)" % (k, now, was))
if len(raised) > MAX_REPORT:
print(" ...(截断,共 %d 条)" % len(raised))
return fail("重生成会抬高 %d 条登记值——基线只准因还债下降,抬高=以现状为尺默认恶化;"
"确属阈值口径变更或经用户批准须加 --force 显式确认" % len(raised))
else: else:
print("[gdstyle_gate] 旧基线不存在,跳过对账(首次落位)") print("[gdstyle_gate] 旧基线不存在,跳过对账(首次落位)")
write_baseline(current) write_baseline(current)
@@ -191,15 +208,15 @@ def run_gate() -> int:
red = False red = False
if new: if new:
for k in new[:MAX_REPORT]: for k in new[:MAX_REPORT]:
print(" NEW " + k) print(" FAIL 新增 " + k)
if len(new) > MAX_REPORT: if len(new) > MAX_REPORT:
print(" ...(截断,共 %d 条)" % len(new)) print(" FAIL ...(截断,共 %d 条)" % len(new))
red = True red = True
if worsened: if worsened:
for k in worsened[:MAX_REPORT]: for k in worsened[:MAX_REPORT]:
print(" WORSENED %s = %d(基线登记 %d)" % (k, current[k], base[k])) print(" FAIL 恶化 %s = %d(基线登记 %d)" % (k, current[k], base[k]))
if len(worsened) > MAX_REPORT: if len(worsened) > MAX_REPORT:
print(" ...(截断,共 %d 条)" % len(worsened)) print(" FAIL ...(截断,共 %d 条)" % len(worsened))
red = True red = True
if red: if red:
return fail("基线外新违规 %d 条/恶化 %d 条——新码须修到基线口径内,恶化须还债或经用户批准随批重生成基线" return fail("基线外新违规 %d 条/恶化 %d 条——新码须修到基线口径内,恶化须还债或经用户批准随批重生成基线"
+26
View File
@@ -0,0 +1,26 @@
"""解析 Godot 的 user:// 根目录——与 game/override.cfg 的隔离口径同一份。
无 override.cfg(主工作区)→ %APPDATA%/Godot/app_userdata/<project.godot 的 config/name>;
有 override.cfg(代码面 worktree,由 tools/setup_worktree_userdir.sh 生成)→ %APPDATA%/<custom_user_dir_name>。
"""
import os
import re
from pathlib import Path
def _project_name(repo_root: Path) -> str:
text = (Path(repo_root) / "game" / "project.godot").read_text(encoding="utf-8", errors="replace")
m = re.search(r'^config/name="([^"]*)"', text, re.M)
return m.group(1) if m else "DungeonSweeperVerticalSlice"
def resolve_user_dir(repo_root: Path) -> Path:
base = Path(os.environ.get("APPDATA") or (Path.home() / "AppData" / "Roaming"))
override = Path(repo_root) / "game" / "override.cfg"
if override.is_file():
text = override.read_text(encoding="utf-8", errors="replace")
custom = re.search(r'^config/use_custom_user_dir=true\s*$', text, re.M)
name = re.search(r'^config/custom_user_dir_name="([^"]*)"', text, re.M)
if custom and name and name.group(1):
return base / name.group(1)
return base / "Godot" / "app_userdata" / _project_name(Path(repo_root))
+3 -2
View File
@@ -24,18 +24,19 @@
import argparse import argparse
import hashlib import hashlib
import json import json
import os
import re import re
import subprocess import subprocess
import sys import sys
import time import time
from pathlib import Path from pathlib import Path
from godot_user_dir import resolve_user_dir
ROOT = Path(__file__).resolve().parent.parent ROOT = Path(__file__).resolve().parent.parent
GODOT = r"D:\code\godot\Godot_v4.7.2-stable_win64_console.exe" GODOT = r"D:\code\godot\Godot_v4.7.2-stable_win64_console.exe"
GAME_DIR = ROOT / "game" GAME_DIR = ROOT / "game"
ANCHOR_PATH = ROOT / "game" / "tests" / "evidence" / "parity" / "anchor.json" ANCHOR_PATH = ROOT / "game" / "tests" / "evidence" / "parity" / "anchor.json"
USER_LOGS = Path(os.environ.get("APPDATA", "")) / "Godot" / "app_userdata" / "DungeonSweeperVerticalSlice" / "logs" USER_LOGS = resolve_user_dir(ROOT) / "logs"
# 变体表:名字 → (seed0, attune, shop)。(N=20/策略 both/route min/feat off 全变体钉死。) # 变体表:名字 → (seed0, attune, shop)。(N=20/策略 both/route min/feat off 全变体钉死。)
VARIANTS = { VARIANTS = {
+8
View File
@@ -41,6 +41,7 @@ GAME="$REPO/game"
STAMP_DIR="$GAME/tests/.gates" STAMP_DIR="$GAME/tests/.gates"
RECEIPT="$STAMP_DIR/receipt.json" RECEIPT="$STAMP_DIR/receipt.json"
BASELINE="$STAMP_DIR/baseline.json" BASELINE="$STAMP_DIR/baseline.json"
QUICK=0 QUICK=0
DOMAIN="full" DOMAIN="full"
for _a in "$@"; do for _a in "$@"; do
@@ -53,6 +54,13 @@ for _a in "$@"; do
esac esac
done done
# 每个代码面 worktree 用独立 Godot user 目录(game/override.cfg 由 setup 脚本自愈生成;主区 no-op)。
# 并行批共用一套夹具、日志与截图目录会互相踩盘造成假红——隔离建不起来就拒跑(fail-closed)。
"$REPO/tools/setup_worktree_userdir.sh" "$REPO" || {
echo "[gates] FAIL — user 目录隔离未建立,拒绝在共享目录上跑闸。"
exit 1
}
# 控制台编码:本仓库闸件输出中文与 ✅/✗ 符号。Windows GBK 控制台下 python 打印这些字符会抛 # 控制台编码:本仓库闸件输出中文与 ✅/✗ 符号。Windows GBK 控制台下 python 打印这些字符会抛
# UnicodeEncodeError——待办清单/10 两闸直接崩(假红)、09 的 FAIL 行被写成乱码字节(指纹漂移, # UnicodeEncodeError——待办清单/10 两闸直接崩(假红)、09 的 FAIL 行被写成乱码字节(指纹漂移,
# 与基线比必判「红项清单变化」假红)、末尾汇总自身崩掉打不出结论。 # 与基线比必判「红项清单变化」假红)、末尾汇总自身崩掉打不出结论。
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env bash
# 代码面 worktree 的 Godot user 目录隔离:给每个 worktree 生成 game/override.cfg,
# 让该区的 user:// 落到独立目录(夹具、日志、截图产物不再与其他区共用)。
# 主工作区不生成——真实存档与试玩进度保持原位。
#
# 用法:tools/setup_worktree_userdir.sh [仓库根](缺省=本脚本所在仓库)
# 幂等:内容一致即不重写;退出码 0=已隔离或主区免隔离,1=生成失败(闸壳据此拒跑)
set -u
ROOT="$(cd "${1:-$(dirname "$0")/..}" && pwd)"
MAIN_WT="$(git -C "$ROOT" worktree list --porcelain 2>/dev/null | sed -n 's/^worktree //p' | head -1)"
if [ -z "$MAIN_WT" ]; then
echo "[userdir] 取不到主工作区路径(git worktree list 为空)" >&2
exit 1
fi
if [ "$(cd "$ROOT" && pwd -P)" = "$(cd "$MAIN_WT" && pwd -P)" ]; then
echo "[userdir] 主工作区:不隔离(真实档保持原位)"
exit 0
fi
BASE="$(basename "$ROOT" | tr -d '\n' | tr -c 'A-Za-z0-9_-' '_')"
ABS="$(cd "$ROOT" && pwd -P)"
if command -v sha1sum >/dev/null 2>&1; then
FP="$(printf '%s' "$ABS" | sha1sum | cut -c1-8)"
else
FP="$(printf '%s' "$ABS" | cksum | tr -d ' ' | cut -c1-8)"
fi
NAME="DungeonSweeper_${BASE}_${FP}"
CFG="$ROOT/game/override.cfg"
CONTENT="[application]
config/use_custom_user_dir=true
config/custom_user_dir_name=\"$NAME\"
"
if [ -f "$CFG" ] && printf '%s' "$CONTENT" | cmp -s - "$CFG"; then
echo "[userdir] 已隔离:$NAME"
exit 0
fi
mkdir -p "$ROOT/game" 2>/dev/null
if ! printf '%s' "$CONTENT" >"$CFG"; then
echo "[userdir] 写 $CFG 失败" >&2
exit 1
fi
echo "[userdir] 已隔离:$NAME"