工程(user目录隔离): 每个代码面 worktree 独立 Godot user 目录+系统测试 V3d 缺帧修正

- 机制: tools/setup_worktree_userdir.sh 在建区后生成 game/override.cfg(use_custom_user_dir + 唯一名 DungeonSweeper_<区名>_<绝对路径 sha1 前 8>), 主工作区 no-op 保持真实档原位; .gitignore 收 game/override.cfg
- 闸壳: run_all_gates.sh 参数解析后先自检/自愈本区隔离, 建不起来 exit 1(fail-closed)——并行批共用一套夹具/日志/截图目录会互相踩盘, 是假红的根源
- 读取面: 新增 tools/godot_user_dir.py 统一解析(有 override→custom dir, 无→默认 appdata/Godot/app_userdata/<config/name>); parity_anchor.py 与 analyze_playtest.py 的硬编码 APPDATA 路径改为调用它
- 流程: process-conventions §1 第 1 条(建区即跑 setup)与 §3 环境基线第 4 条(闸壳自检); iter 技能冷检出三件套→四件套(.zcode/.qoder 两份本机副本已同步, 不入 git)
- 系统测试 V3d: 隔离后空 user 目录(无 settings.cfg)使 reduced_motion 取默认 false, 实查暴露 V3d 真实缺陷——测试同步驱动整场战斗后同帧 flush, RowPacer._was_pumping 从未置真, 演播后排窗的 pump_ended 边沿不触发, _end_overlay 永不打开; 旧绿只因本机 settings.cfg reduced=true 让排播走 consume_now 快路绕过队列。补一帧 await process_frame 让泵先观测到非空队列再 flush, 恢复真实边沿路径
- 取证: 隔离空目录下 00 冒烟 17/17、02 单元与集成 62 套全绿、03 探针 --click 全绿; 01 系统测试修正前 70/1 红(仅 V3d)、修正后 reduced=true 与 false 两档均 70/70 绿
This commit is contained in:
DKChan committed 2026-10-27 05:23:13 +08:00
1 parent 8452162665
commit 8bfd249402
8 files changed
+92 -6

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. **涉数值改动先采改前读数**——与改后同随机种子配对比较并写明逐项比对口径;无数值改动禁跑模拟
**实施** **实施**
+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("-"):
+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"