From a06d90aac25c8121884b7e6041323e4b81de17ec Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Wed, 28 Oct 2026 02:46:32 +0800 Subject: [PATCH 1/8] =?UTF-8?q?=E7=99=BB=E8=AE=B0(=E5=B7=A5=E7=A8=8B?= =?UTF-8?q?=E6=94=B9=E5=8A=A8=C2=B7=E5=BE=85=E6=89=A7=E8=A1=8C):=2002=20?= =?UTF-8?q?=E9=97=B8=E9=80=89=E8=B7=91=E8=BF=87=E6=BB=A4=E5=99=A8=E2=80=94?= =?UTF-8?q?=E2=80=94run=5Fall.gd=20=E5=8A=A0=20--only=20=E9=80=89=E8=B7=91?= =?UTF-8?q?=E5=8F=82=E6=95=B0=E2=80=94=E2=80=94=E5=BC=80=E5=8F=91=E6=9C=9F?= =?UTF-8?q?=E6=9C=80=E7=AA=84=E7=A1=AE=E8=AE=A4=E7=BC=BA=E6=8C=89=E5=9F=9F?= =?UTF-8?q?=E9=80=89=E8=B7=91=E6=89=A7=E8=A1=8C=E8=BD=BD=E4=BD=93(?= =?UTF-8?q?=E5=8D=A1=E5=86=85=E5=8F=AA=E6=B5=8B=E8=87=AA=E5=B7=B1=E6=94=B9?= =?UTF-8?q?=E7=9A=84=E4=B8=9C=E8=A5=BF=E6=97=A0=E8=90=BD=E5=9C=B0=E6=89=8B?= =?UTF-8?q?=E6=AE=B5,64=20=E5=A5=97=E4=BB=B6=E9=80=90=E4=B8=AA=E8=B5=B7=20?= =?UTF-8?q?Godot=20=E5=AD=90=E8=BF=9B=E7=A8=8B=E5=85=A8=E9=87=8F=E8=B7=91)?= =?UTF-8?q?;=E9=80=89=E8=B7=91=3D=E5=AF=B9=E5=A5=97=E4=BB=B6=20res://=20?= =?UTF-8?q?=E8=B7=AF=E5=BE=84=E5=AD=90=E4=B8=B2=E5=8C=B9=E9=85=8D,?= =?UTF-8?q?=E9=9B=B6=E5=91=BD=E4=B8=AD/=E7=A9=BA=E5=80=BC=3D=E7=94=A8?= =?UTF-8?q?=E6=B3=95=E9=94=99=20quit(2)=20fail-closed,FILTERED=20RUN=20?= =?UTF-8?q?=E6=A8=AA=E5=B9=85+=E6=97=A5=E5=BF=97=E5=8F=A6=E5=90=8D=20run?= =?UTF-8?q?=5Fall=5Ffilter=5F*,=E9=BB=98=E8=AE=A4=E8=B7=AF=E5=BE=84?= =?UTF-8?q?=E9=80=90=E5=AD=97=E8=8A=82=E4=B8=8D=E5=8F=98,=E9=80=89?= =?UTF-8?q?=E8=B7=91=E4=BB=85=E9=99=90=E7=9B=B4=E8=B7=91=E5=BD=A2=E6=80=81?= =?UTF-8?q?=E6=B0=B8=E4=B8=8D=E4=BA=A7=E5=9B=9E=E6=89=A7;=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E8=A3=81=202026-10-28=20=E5=BC=80=E5=8F=91=E6=9C=9F?= =?UTF-8?q?=E9=9B=B6=E4=BB=A3=E7=A0=81=E9=AA=8C=E8=AF=81=E7=9B=B4=E8=B7=91?= =?UTF-8?q?(=E5=88=A4=E7=BA=A2=E7=BB=BF=E6=AD=A3=E7=A1=AE=E6=80=A7?= =?UTF-8?q?=E9=A6=96=E6=AC=A1=E5=AE=9E=E8=AF=81=3D=E5=AE=9E=E9=99=85?= =?UTF-8?q?=E4=BD=BF=E7=94=A8),=E6=96=87=E6=A1=A3=E5=90=8C=E6=AD=A5?= =?UTF-8?q?=E9=9D=A2=3Dtests/README=20=E5=B7=A5=E5=85=B7=E9=80=9F=E6=9F=A5?= =?UTF-8?q?=E8=A1=8C+architecture=20run=5Fall=20=E8=A1=8C(=E5=90=AB?= =?UTF-8?q?=E4=B8=A4=E5=A4=84=E5=A5=97=E4=BB=B6=E6=95=B0=E6=BC=82=E7=A7=BB?= =?UTF-8?q?=E8=AE=A2=E6=AD=A3=2040=E2=86=9264)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 待办清单.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/待办清单.md b/待办清单.md index fc8c0dd8..ffcce662 100644 --- a/待办清单.md +++ b/待办清单.md @@ -545,6 +545,15 @@ **状态**:待执行 +#### 02 闸选跑过滤器——run_all.gd 加 --only 选跑参数(2026-10-28 登记,来源=会话提及) + +**实况**:证据=2026-10-28 会话讨论与用户裁决(开发期全量跑测代价过大;开发期零代码验证直跑,验证责任收缩为文档校验+批末例行闸)。改法=game/tests/run_all.gd 加选跑态:`-- --only=<词[,词…]>` 对套件 res:// 路径做子串匹配只跑命中套件——「卡内只测自己改的东西」(2026-10-22/25 频率规则)此前缺按域选跑的执行载体,64 套件逐个起 Godot 子进程是开发期最窄确认的实际成本。语义五条:①零命中/空值=用法错 quit(2)(fail-closed,禁把零套件当绿);②选跑态打印 FILTERED RUN 横幅(选跑 N/M 套件),输出自明非全量;③选跑日志另名 run_all_filter_*,不覆写全量跑批日志;④默认路径(无参数)逐字节现行行为——批末全量闸不带参数不经选跑分支;⑤选跑仅限直跑形态、永不经 run_all_gates.sh 入口——不写 receipt.json,合回凭据零污染。验证口径(用户裁 2026-10-28):开发期零代码验证直跑,选跑分支判红绿正确性首次实证=实际使用;批末 --code 闸照跑(02 闸实跑默认路径,语法/解析错误必红)。文档同步面(合回后一笔直提):game/tests/README.md 工具速查行补用法+套件数漂移订正(写 40 实为 64);design/engineering/architecture/architecture.md 测试与质量域 run_all 行补选跑态一句+同句计数订正(39 单元+1 集成,实为 61+3)。 +- 直改落点:game/tests/run_all.gd——触及代码面(game/ 下非 .md 行为载体),按车道铁律走 worktree 批、批末 --code 闸 + +**改动面**:工具代码+文档 + +**状态**:待执行 + #### 美术资产链路审计两项 P0 — 待用户裁决(2026-10-09 登记,来源=审计发现) **实况**:**P0①(文档面)**:`design/art/art-bible.md` 第 68/527 行与 art-status-checklist 第 13 行至今仍引用已丢失的冻结样张 sample-batch-v3-final-2026-09-30.png(磁盘实测零命中),死指涉待修复。**P0②(美术面)**:主角棋盘小人未走像素管线(插值糊图),重产事项未在任何登记处在册。审计报告 HTML 件从未进版本库、现已删除,两项 P0 的现状以本条正文为准。裁=修复死指涉+登记重产,或判过期随旧管线退役。2026-10-13 用户令随「UI 美化三层诊断」登记:本条属第三层「观感提升」,Alpha 阶段需完成。 From 6caf56b5c1ce542e7c997b47ac0746116696e5de Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Wed, 28 Oct 2026 02:49:12 +0800 Subject: [PATCH 2/8] =?UTF-8?q?=E5=B7=A5=E7=A8=8B(02=E9=97=B8=E9=80=89?= =?UTF-8?q?=E8=B7=91=E8=BF=87=E6=BB=A4=E5=99=A8):=20run=5Fall.gd=20?= =?UTF-8?q?=E5=8A=A0=20--only=20=E9=80=89=E8=B7=91=E6=80=81=E2=80=94?= =?UTF-8?q?=E2=80=94OS.get=5Fcmdline=5Fuser=5Fargs=20=E8=A7=A3=E6=9E=90=20?= =?UTF-8?q?--only=3D<=E8=AF=8D[,=E8=AF=8D=E2=80=A6]>=20=E5=AF=B9=E5=A5=97?= =?UTF-8?q?=E4=BB=B6=20res://=20=E8=B7=AF=E5=BE=84=E5=AD=90=E4=B8=B2?= =?UTF-8?q?=E5=8C=B9=E9=85=8D=E5=8F=AA=E8=B7=91=E5=91=BD=E4=B8=AD=E5=A5=97?= =?UTF-8?q?=E4=BB=B6;=E9=9B=B6=E5=91=BD=E4=B8=AD/=E7=A9=BA=E5=80=BC=3D?= =?UTF-8?q?=E7=94=A8=E6=B3=95=E9=94=99=20quit(2)=20fail-closed=20=E7=A6=81?= =?UTF-8?q?=E9=9B=B6=E5=A5=97=E4=BB=B6=E5=BD=93=E7=BB=BF;=E9=80=89?= =?UTF-8?q?=E8=B7=91=E6=80=81=E6=89=93=E5=8D=B0=20FILTERED=20RUN=20?= =?UTF-8?q?=E6=A8=AA=E5=B9=85+=E6=97=A5=E5=BF=97=E5=8F=A6=E5=90=8D=20run?= =?UTF-8?q?=5Fall=5Ffilter=5F*(=E4=B8=8D=E8=A6=86=E5=86=99=E5=85=A8?= =?UTF-8?q?=E9=87=8F=E8=B7=91=E6=89=B9=E6=97=A5=E5=BF=97);=E5=AE=9E?= =?UTF-8?q?=E8=B7=91=E6=95=B0=E6=A0=A1=E9=AA=8C=E5=AF=B9=E8=BF=87=E6=BB=A4?= =?UTF-8?q?=E5=90=8E=E6=B8=85=E5=8D=95,=E5=85=A8=E9=87=8F=E6=80=81?= =?UTF-8?q?=E6=9D=A1=E7=9B=AE=E4=B8=8E=E9=A1=BA=E5=BA=8F=E9=9B=B6=E5=8F=98?= =?UTF-8?q?=E5=8C=96;=E9=BB=98=E8=AE=A4=E8=B7=AF=E5=BE=84(=E6=97=A0?= =?UTF-8?q?=E5=8F=82=E6=95=B0)=E9=80=90=E5=AD=97=E8=8A=82=E7=8E=B0?= =?UTF-8?q?=E8=A1=8C=E8=A1=8C=E4=B8=BA=E2=80=94=E2=80=94=E6=89=B9=E6=9C=AB?= =?UTF-8?q?=E5=85=A8=E9=87=8F=E9=97=B8=E4=B8=8D=E5=B8=A6=E5=8F=82=E6=95=B0?= =?UTF-8?q?=E4=B8=8D=E7=BB=8F=E9=80=89=E8=B7=91=E5=88=86=E6=94=AF=E3=80=82?= =?UTF-8?q?=E9=AA=8C=E8=AF=81=E5=8F=A3=E5=BE=84=3D=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E8=A3=81=202026-10-28:=E5=BC=80=E5=8F=91=E6=9C=9F=E9=9B=B6?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E9=AA=8C=E8=AF=81=E7=9B=B4=E8=B7=91,?= =?UTF-8?q?=E9=80=89=E8=B7=91=E5=88=86=E6=94=AF=E5=88=A4=E7=BA=A2=E7=BB=BF?= =?UTF-8?q?=E6=AD=A3=E7=A1=AE=E6=80=A7=E9=A6=96=E6=AC=A1=E5=AE=9E=E8=AF=81?= =?UTF-8?q?=3D=E5=AE=9E=E9=99=85=E4=BD=BF=E7=94=A8;=E6=9C=AC=E6=89=B9?= =?UTF-8?q?=E4=BB=85=E5=81=9A=E7=A7=92=E7=BA=A7=20--check-only=20=E8=AF=AD?= =?UTF-8?q?=E6=B3=95=E8=87=AA=E6=A3=80(=E4=B8=8D=E8=B7=91=E5=A5=97?= =?UTF-8?q?=E4=BB=B6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- game/tests/run_all.gd | 61 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 52 insertions(+), 9 deletions(-) diff --git a/game/tests/run_all.gd b/game/tests/run_all.gd index 20b4ee0c..908b4c44 100644 --- a/game/tests/run_all.gd +++ b/game/tests/run_all.gd @@ -1,5 +1,8 @@ # tests/run_all.gd — 单元测试统一入口(双载体的逻辑测试调用点,套件 manifest 协议 D2 AC-2b) # 用法:godot --headless --path game --script res://tests/run_all.gd (本地闸 02 闸包装外部超时:timeout 120s …) +# 选跑态(开发期直跑专用):追加 `-- --only=<词[,词…]>` ——对套件 res:// 路径做子串匹配,只跑命中套件; +# 零命中/空值=用法错 quit(2)。选跑态仅供开发期最窄确认,不产闸回执、不作验收凭据; +# 批末全量闸不带参数不经选跑分支——默认(无参数)行为逐字节不变。 # 协议:套件清单逐条"套件 → 期望退出码";致命类独立进程(expect=1)。 # 执行形态:逐套件起独立 Godot 子进程实跑 #(OS.get_executable_path() 解析本引擎二进制——--script 模式无 autoload/全局标识符, @@ -13,24 +16,51 @@ const ERROR_MARKERS: Array[String] = ["SCRIPT ERROR", "Parse Error"] func _init() -> void: - print("[run_all] unit/contract suite entry — %d suites" % _suites().size()) + # 选跑态解析:--only=<词[,词…]>(经 -- 分隔的 user args)——对套件 res:// 路径做子串匹配。 + # 空值/零命中=用法错 quit(2)(fail-closed:禁把零套件当绿);选跑态仅供开发期最窄确认, + # 不产闸回执、不作验收凭据;批末全量闸不带参数,不走此分支。 + var only_terms := PackedStringArray() + var has_only := false + for a in OS.get_cmdline_user_args(): + if a.begins_with("--only="): + has_only = true + var term := a.trim_prefix("--only=").strip_edges() + if term != "": + only_terms.append(term) + if has_only and only_terms.is_empty(): + push_error("[run_all] --only= 空值——用法错(选跑态禁把零套件当全量)") + quit(2) + return + var manifest := _suites() + var suites := manifest + var filtered := false + if has_only: + suites = _filter_suites(manifest, only_terms) + if suites.is_empty(): + push_error("[run_all] FILTERED RUN — --only=「%s」零命中(manifest 共 %d 套件)——用法错" % [",".join(only_terms), manifest.size()]) + quit(2) + return + filtered = true + print("[run_all] FILTERED RUN — 选跑 %d/%d 套件(--only=%s)——仅供开发期最窄确认,不产闸回执、不作验收凭据" % [suites.size(), manifest.size(), ",".join(only_terms)]) + print("[run_all] unit/contract suite entry — %d suites" % suites.size()) if DisplayServer.get_name() != "headless": push_warning("[run_all] 建议以 --headless 运行(D2 冒烟协议)") var failures := 0 var ran := 0 - for suite in _suites(): - failures += _run_suite(suite, ran) + for suite in suites: + failures += _run_suite(suite, ran, filtered) ran += 1 - # 实跑套件数校验:manifest 有条目却被静默跳过(早退/continue)必须判红,不能只凭 failures。 - if ran != _suites().size(): - push_error("[run_all] 实跑套件数 %d ≠ manifest 条目数 %d——有套件未跑" % [ran, _suites().size()]) + # 实跑套件数校验:清单有条目却被静默跳过(早退/continue)必须判红,不能只凭 failures。 + # 选跑态对过滤后清单校验;全量态与原 manifest 条目数同一。 + if ran != suites.size(): + push_error("[run_all] 实跑套件数 %d ≠ 清单条目数 %d——有套件未跑" % [ran, suites.size()]) quit(1) return if failures > 0: push_error("[run_all] %d 个套件失败" % failures) quit(1) return - print("[run_all] all green (%d suites)" % _suites().size()) + print("[run_all] all green (%d suites%s)" % [suites.size(), ",filtered" if filtered else ""]) quit(0) @@ -111,7 +141,7 @@ func _suites() -> Array: ] -func _run_suite(suite: Dictionary, idx: int) -> int: +func _run_suite(suite: Dictionary, idx: int, filtered: bool = false) -> int: var path: String = suite.get("path", "") var expect: int = int(suite.get("expect", 0)) if path.is_empty(): @@ -125,7 +155,7 @@ func _run_suite(suite: Dictionary, idx: int) -> int: # 子进程输出只能落盘再读:OS.execute 的 output 管道在本机 Godot/Windows 组合下恒为空 #(实测 0 行),故整条命令交给 cmd /c 并重定向到日志,退出码由 cmd 原样回传。 var proj := ProjectSettings.globalize_path("res://") - var log := proj + "/tests/.gates/run_all_%02d.log" % idx + var log := proj + "/tests/.gates/run_all%s_%02d.log" % ["_filter" if filtered else "", idx] DirAccess.make_dir_recursive_absolute(proj + "/tests/.gates") var cmd := '"%s" --headless --path "%s" --script "%s" > "%s" 2>&1' % [bin, proj, path, log] var out := PackedStringArray() @@ -148,6 +178,19 @@ func _run_suite(suite: Dictionary, idx: int) -> int: return 0 +# 选跑过滤:对套件 res:// 路径做子串匹配,任一词命中即选入,词序无关。 +# 只在选跑态调用——全量态走原 manifest,条目与顺序零变化。 +func _filter_suites(manifest: Array, terms: PackedStringArray) -> Array: + var out: Array = [] + for suite in manifest: + var path: String = suite.get("path", "") + for t in terms: + if path.contains(t): + out.append(suite) + break + return out + + # 引擎二进制必须取 console 版:OS.get_executable_path() 返回的是 GUI 子系统 exe #(Godot_v4.7.2-stable_win64.exe),它起的子进程 stdout/stderr 句柄无效——输出全丢, # 脚本错误扫描就成了扫空气。GODOT_BIN 优先,其次按命名推导 *_console.exe。 From e55bc0c14200824e0d667f40d73a1406fda1d1f6 Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Wed, 28 Oct 2026 03:01:49 +0800 Subject: [PATCH 3/8] =?UTF-8?q?=E5=B7=A5=E7=A8=8B(02=E9=80=89=E8=B7=91?= =?UTF-8?q?=E8=BF=87=E6=BB=A4=E5=99=A8=E6=94=B6=E7=BC=96):=20=E6=96=87?= =?UTF-8?q?=E6=A1=A3=E5=90=8C=E6=AD=A5=E2=80=94=E2=80=94game/tests/README?= =?UTF-8?q?=20=E8=A1=A5=20--only=20=E9=80=89=E8=B7=91=E7=94=A8=E6=B3=95(?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=E9=80=9F=E6=9F=A5=E8=A1=8C+Running=20Tests?= =?UTF-8?q?=20=E5=91=BD=E4=BB=A4=E8=A1=8C)=EF=BC=8B=E5=A5=97=E4=BB=B6?= =?UTF-8?q?=E6=95=B0=E6=BC=82=E7=A7=BB=E8=AE=A2=E6=AD=A3(40=E2=86=9264,39?= =?UTF-8?q?=20=E5=8D=95=E5=85=83+1=20=E9=9B=86=E6=88=90=E2=86=9261+3,?= =?UTF-8?q?=E9=9B=86=E6=88=90=201=E2=86=923);architecture.md=20=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E4=B8=8E=E8=B4=A8=E9=87=8F=E5=9F=9F=20run=5Fall=20?= =?UTF-8?q?=E8=A1=8C=E8=A1=A5=E9=80=89=E8=B7=91=E6=80=81=E7=99=BB=E8=AE=B0?= =?UTF-8?q?=EF=BC=8B=E5=90=8C=E6=AC=BE=E8=AE=A1=E6=95=B0=E8=AE=A2=E6=AD=A3?= =?UTF-8?q?(2026-10-28=20=E5=AE=9E=E6=95=B0);=E5=BE=85=E5=8A=9E=E6=B8=85?= =?UTF-8?q?=E5=8D=95=E3=80=8C02=20=E9=97=B8=E9=80=89=E8=B7=91=E8=BF=87?= =?UTF-8?q?=E6=BB=A4=E5=99=A8=E3=80=8D=E6=9D=A1=E7=9B=AE=E5=AE=8C=E6=88=90?= =?UTF-8?q?=E6=95=B4=E6=9D=A1=E5=88=A0=E9=99=A4=E2=80=94=E2=80=94=E6=89=B9?= =?UTF-8?q?=E5=B7=B2=E5=90=88=E5=9B=9E=20master(fe62463b)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- design/engineering/architecture/architecture.md | 2 +- game/tests/README.md | 7 ++++--- 待办清单.md | 9 --------- 3 files changed, 5 insertions(+), 13 deletions(-) diff --git a/design/engineering/architecture/architecture.md b/design/engineering/architecture/architecture.md index b283a23b..97ed913e 100644 --- a/design/engineering/architecture/architecture.md +++ b/design/engineering/architecture/architecture.md @@ -226,7 +226,7 @@ |---|---|---|---|---| | **run_smoke**(冒烟测试运行器) | `tools/run_smoke.py` | 冒烟测试总入口与判定权:headless 以项目运行目标拉起 `res://tests/smoke_boot.tscn`(极小启动场景——autoload 在位、配置表全载、主屏实例化,零逻辑判定)+ 三闸判定 | 进程退出码(0=PASS / 1=FAIL) | 三闸齐备、判定无洞:①退出码 = 0;②摘要行「[smoke] 摘要:N 项检查,0 项失败」在案且失败数为 0;③输出扫 `SCRIPT ERROR` / `Parse Error` 标记行。外部 wall-clock 超时在位(超时 = FAIL) | | **run_system_test**(系统测试运行器) | `tools/run_system_test.py` | 系统测试总入口:headless 跑真实玩法路径断言面(宿主=`res://tests/system_test.gd`)+ 三闸判定(口径同 run_smoke,摘要行兼容两种宿主格式) | 进程退出码(0=PASS / 1=FAIL) | 三闸齐备(同 run_smoke:退出码/摘要失败数/脚本错误扫描;wall-clock 超时在位) | -| **run_all**(单元与集成测试) | `game/tests/run_all.gd` | 40 套统一入口(39 单元+1 集成,2026-10-22 实数):套件 manifest 逐条「套件 → 期望退出码」,致命类 expect=1 独立子进程实跑;`negative` 字段标负面套件(仅豁免错误扫描一闸) | 逐套件结果 + 汇总退出码(任一套件失败 quit(1)) | 套件级退出码协议在位;三硬闸齐备(入口内扫子套件输出 + 编排层扫 `unit.log`;`Kit.finish` 零断言返回专用码 2)。子进程输出须经系统命令解释器重定向落盘再读——`OS.execute` 的输出管道在本机恒空 | +| **run_all**(单元与集成测试) | `game/tests/run_all.gd` | 64 套统一入口(61 单元+3 集成,2026-10-28 实数):套件 manifest 逐条「套件 → 期望退出码」,致命类 expect=1 独立子进程实跑;`negative` 字段标负面套件(仅豁免错误扫描一闸);选跑态 `--only=<词[,词…]>`(直跑形态对套件路径子串匹配只跑命中套件,零命中=用法错 quit(2),仅供开发期最窄确认——不产闸回执、不作验收凭据,批末全量闸不经选跑分支) | 逐套件结果 + 汇总退出码(任一套件失败 quit(1)) | 套件级退出码协议在位;三硬闸齐备(入口内扫子套件输出 + 编排层扫 `unit.log`;`Kit.finish` 零断言返回专用码 2)。子进程输出须经系统命令解释器重定向落盘再读——`OS.execute` 的输出管道在本机恒空 | | **probe_gate**(场景探针) | `tools/probe_gate.sh` | headless 场景探针段编排:默认段 ui_input_probe / ui_struct_probe / layout_probe / ui_contrast_probe / **screen_flow_probe**(屏壳切换),`--click` 加跑 ui_click_probe,`--full` 加跑 ui_probe / ui_tune_probe / t17_defer_probe / fx_probe;孤儿 Godot 预清 + 批末残留清点(必须 0) | 段级 FAIL 输出 + 汇总退出码 | 每段 timeout 守卫在位(挂死 124/137 = FAIL,不静默);判定统一走 `verdict` 函数——退出码为唯一权威判据,PASS 串降级为附加判据,逐段跑 `tools/error_scan.py` | | **render_gate**(视觉 golden) | `tools/render_gate.sh` | 窗口化渲染闸:①shot_probe 七屏截图 → shot_checker A-D 语义断言;②fx_shot_probe / board_shot_probe 取证(超时/零字节/文件缺失任一 = FAIL);③棋盘 golden 逐像素容差比对(差 ≤16 / changed ≤2%,provisional)+ 菜单 golden;④批末孤儿残留清零 | 段级 FAIL 输出 + 汇总退出码 | 查退出码在位(每段 timeout 守卫 + 退出码/取证文件双检);**SCRIPT ERROR 扫描缺失**;断言数下限不适用(判定主体 = golden 容差与取证文件存在性,非断言计数) | | **static_scan**(静态扫描域) | `game/tests/static_scan.sh` | 静态扫描域 S1–S10 可执行载体(grep 规则 + `tools/audit_type_hints.py`〔S9〕+ `tools/audit_file_headers.py`〔S10〕) | fail 聚合退出码(任一硬失败规则命中即 exit 1) | 查退出码在位;SCRIPT ERROR 扫描与断言数下限不适用(逐规则命中即红,无运行时错误面与断言计数);S3 现为 warning 级、不参与 fail 判定(见下表) | diff --git a/game/tests/README.md b/game/tests/README.md index 11d14a72..e0f38616 100644 --- a/game/tests/README.md +++ b/game/tests/README.md @@ -58,7 +58,7 @@ tests/ |---|---|---|---| | `tools/run_smoke.py`(冒烟测试) | 这版能否启动、能否加载表、能否进主屏——零逻辑判定 | 退出码+脚本错误扫描 | 开机路径/表加载改动后的快速自检;收口由闸编排 | | `tools/run_system_test.py`(系统测试) | 真实玩法路径的行为断言面(推理→战斗→结算→进度推进) | 退出码+脚本错误扫描 | 玩法链路改动的调试期确认 | -| `tests/run_all.gd`(单元与集成) | 公式/状态机/契约/跨系统闭环(40 套件 manifest) | 退出码+三硬闸(见下) | 新套件单跑调试;逻辑改动的最小单元复跑 | +| `tests/run_all.gd`(单元与集成) | 公式/状态机/契约/跨系统闭环(64 套件 manifest) | 退出码+三硬闸(见下) | 新套件单跑调试;逻辑改动的最小单元复跑;开发期选跑——追加 `-- --only=<词[,词…]>` 只跑路径命中词的套件(零命中=用法错 exit 2;选跑态仅供开发期最窄确认,不产闸回执、不作验收凭据) | | `tools/probe_gate.sh`(场景探针,`--click`/`--full` 两模式) | UI 可达性、可见性、状态——真实实例化场景+真实输入路径 | 探针自检+退出码 | 新增 UI 区域的配套探针直跑与断言复核 | | `tools/render_gate.sh`(渲染 golden) | 画面基线——截图与基线图逐像素容差比对 | golden 比对脚本(依赖 PIL) | 主题/素材/排版类改动;窗口化运行,需开显示器 | | `game/tests/static_scan.sh`(S 域) | 项目纪律+第三方质量规则:RNG 单点/core 纯度/类型标注/文件头/注释禁词/旧符号残留;S13=gdstyle 质量规则(重复 load、循环内 await、循环内分配、不可达代码等真缺陷类;基线键=文件|规则|函数名(类/文件级=文件|规则,无数字条目带消息),数字漂移不假红,同键指标恶化超基线登记值即红) | 退出码(逐规则 FAIL 行) | 改产品码前后的快速自检;新增纪律先补扫描规则再修码 | @@ -75,6 +75,7 @@ python tools/run_smoke.py # 单元与集成测试(闸 02)——Windows 须用 console 版 exe 取 stdout;工作目录 = game/(含 project.godot 那一层) godot --headless --script tests/run_all.gd # 统一入口(本地闸 02 闸调用点) +godot --headless --script tests/run_all.gd -- --only=battle_settlement # 选跑态——只跑路径含该词的套件(开发期最窄确认;零命中=用法错 exit 2,不产闸回执) godot --headless --script tests/unit/example_test.gd # 系统测试(闸 01)——过渡期宿主 smoke_all.gd;分钟级 @@ -87,8 +88,8 @@ godot --headless --path . res://probe/.tscn timeout 120s godot --headless --script tests/run_all.gd ``` -> 套件 manifest 现况:`_suites()` 登记 **40 条**(39 单元+1 集成,2026-10-22 实数;口径=manifest 条目数, -> 随套件增减浮动)——data_config(ConfigDB 加载/进度坐标 ceil 解析/fatal 退出码/统一随机流)、core(秘宝结构冻结/断点续玩/存档槽位/结算五段等)、battle_settlement(DamageMath 纯函数/DamageController 伤害单据五配方)、contracts(系统间接口契约断言)、presentation(节奏队列/奖励浮层/字体资产/地形瓦片集/模态编排等);集成 1 条存 `tests/integration/`。致命类独立进程 `expect=1`;负面套件 `negative=true` 只豁免错误扫描一闸。期望值一律由表读值推导(零玩法数值字面量)、随机只经 RngService 统一流。 +> 套件 manifest 现况:`_suites()` 登记 **64 条**(61 单元+3 集成,2026-10-28 实数;口径=manifest 条目数, +> 随套件增减浮动)——data_config(ConfigDB 加载/进度坐标 ceil 解析/fatal 退出码/统一随机流)、core(秘宝结构冻结/断点续玩/存档槽位/结算五段等)、battle_settlement(DamageMath 纯函数/DamageController 伤害单据五配方)、contracts(系统间接口契约断言)、presentation(节奏队列/奖励浮层/字体资产/地形瓦片集/模态编排等);集成 3 条存 `tests/integration/`。致命类独立进程 `expect=1`;负面套件 `negative=true` 只豁免错误扫描一闸。期望值一律由表读值推导(零玩法数值字面量)、随机只经 RngService 统一流。 **协议要点**(D2 冒烟协议): - `--script` 载体 `extends SceneTree` 以获得 `quit(code)`;成功 `quit(0)`、失败 `quit(1)` diff --git a/待办清单.md b/待办清单.md index ffcce662..fc8c0dd8 100644 --- a/待办清单.md +++ b/待办清单.md @@ -545,15 +545,6 @@ **状态**:待执行 -#### 02 闸选跑过滤器——run_all.gd 加 --only 选跑参数(2026-10-28 登记,来源=会话提及) - -**实况**:证据=2026-10-28 会话讨论与用户裁决(开发期全量跑测代价过大;开发期零代码验证直跑,验证责任收缩为文档校验+批末例行闸)。改法=game/tests/run_all.gd 加选跑态:`-- --only=<词[,词…]>` 对套件 res:// 路径做子串匹配只跑命中套件——「卡内只测自己改的东西」(2026-10-22/25 频率规则)此前缺按域选跑的执行载体,64 套件逐个起 Godot 子进程是开发期最窄确认的实际成本。语义五条:①零命中/空值=用法错 quit(2)(fail-closed,禁把零套件当绿);②选跑态打印 FILTERED RUN 横幅(选跑 N/M 套件),输出自明非全量;③选跑日志另名 run_all_filter_*,不覆写全量跑批日志;④默认路径(无参数)逐字节现行行为——批末全量闸不带参数不经选跑分支;⑤选跑仅限直跑形态、永不经 run_all_gates.sh 入口——不写 receipt.json,合回凭据零污染。验证口径(用户裁 2026-10-28):开发期零代码验证直跑,选跑分支判红绿正确性首次实证=实际使用;批末 --code 闸照跑(02 闸实跑默认路径,语法/解析错误必红)。文档同步面(合回后一笔直提):game/tests/README.md 工具速查行补用法+套件数漂移订正(写 40 实为 64);design/engineering/architecture/architecture.md 测试与质量域 run_all 行补选跑态一句+同句计数订正(39 单元+1 集成,实为 61+3)。 -- 直改落点:game/tests/run_all.gd——触及代码面(game/ 下非 .md 行为载体),按车道铁律走 worktree 批、批末 --code 闸 - -**改动面**:工具代码+文档 - -**状态**:待执行 - #### 美术资产链路审计两项 P0 — 待用户裁决(2026-10-09 登记,来源=审计发现) **实况**:**P0①(文档面)**:`design/art/art-bible.md` 第 68/527 行与 art-status-checklist 第 13 行至今仍引用已丢失的冻结样张 sample-batch-v3-final-2026-09-30.png(磁盘实测零命中),死指涉待修复。**P0②(美术面)**:主角棋盘小人未走像素管线(插值糊图),重产事项未在任何登记处在册。审计报告 HTML 件从未进版本库、现已删除,两项 P0 的现状以本条正文为准。裁=修复死指涉+登记重产,或判过期随旧管线退役。2026-10-13 用户令随「UI 美化三层诊断」登记:本条属第三层「观感提升」,Alpha 阶段需完成。 From 8f5506f7e33eae43c45d105917f474efeed53e87 Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Wed, 28 Oct 2026 03:52:03 +0800 Subject: [PATCH 4/8] =?UTF-8?q?=E7=99=BB=E8=AE=B0(=E5=B7=A5=E7=A8=8B?= =?UTF-8?q?=E6=94=B9=E5=8A=A8=C2=B7=E5=BE=85=E6=89=A7=E8=A1=8C):=20?= =?UTF-8?q?=E4=B8=80=E5=8D=A1=E4=B8=80=E6=8F=90=E4=BA=A4=E4=B8=8A=E5=B2=B8?= =?UTF-8?q?=E5=A5=97=E4=BB=B6/undo=5Fproof=20=E5=B7=A5=E5=85=B7=E5=8C=96/?= =?UTF-8?q?=E5=BD=B1=E5=93=8D=E9=9D=A2=E9=80=89=E9=97=B8=E4=B8=89=E6=9D=A1?= =?UTF-8?q?=E2=80=94=E2=80=942026-10-28=20=E7=94=A8=E6=88=B7=E8=A3=81?= =?UTF-8?q?=E5=9B=9B=E6=89=B9=E5=85=A8=E5=81=9A,=E6=8C=82=E6=AD=BB?= =?UTF-8?q?=E9=98=B2=E6=8A=A4=E6=B2=BF=E7=94=A8=2010-25=20=E5=9C=A8?= =?UTF-8?q?=E5=86=8C=E6=9D=A1=E7=9B=AE=E4=B8=8D=E9=87=8D=E7=99=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 待办清单.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/待办清单.md b/待办清单.md index fc8c0dd8..53d7a51a 100644 --- a/待办清单.md +++ b/待办清单.md @@ -2,6 +2,30 @@ 本清单是流水线的唯一登记处:要做的事先在此登记一条,随流水线段推进改状态,做完整条删除;各系统文档的「待办」节只留一行指路存根,不再自建条目。条目格式(标题=登记元数据含可选优先级,正文=实况/缺口/改动面/状态四属性)与状态机以 [doc-conventions §12.1](design/engineering/doc-conventions.md) 为权威。 +#### 一卡一提交上岸套件——分支重塑工具+合回闸数卡判据+回执树哈希对齐(2026-10-28 登记,来源=会话提及) + +**实况**:证据=迭代 39/40 两个实现会话 44 笔提交中非实装 27 笔(git log 实测),碎片提交与收口文书直接上岸 master 是最大宗。2026-10-28 用户裁落地形态=重塑方案:分支内自由提交、批末 `tools/reshape_cards.py` 按卡末态快照重塑为一卡一笔(可选一笔文档收尾)、合回闸校验卡数与提交格式、回执以内容树哈希对齐(重塑与合并改写 commit 号后仍可判定「合回内容=跑闸内容」)。批末顺序改为「并 master → 重塑 → 跑闸 → 合回」。落点:tools/reshape_cards.py(新建)+tools/run_all_gates.sh(回执加 tree 字段)+.zcode/hooks/worktree-guard.sh(数卡/无杂笔/格式三判据+树对齐)+design/engineering/process-conventions.md §1.2(批末顺序与上岸形态)+.zcode/skills/iter/SKILL.md(同语)。工程改动(工具代码+文档),用户已发令(2026-10-28 四批全做裁决)。 + +**改动面**:工具代码+文档 + +**状态**:待执行 + +#### 撤销证明工具化——undo_proof 自动红绿回执+变异禁令落 test-conventions(2026-10-28 登记,来源=会话提及) + +**实况**:证据=迭代 40 卡① 十例取证人工三连(绿→真撤销复红→还原复绿)且撞 2026-10-24 变异禁令——禁令原文只在 req-card 技能内,test-conventions 指针空挂(iter40 反哺点 B3 在册)。2026-10-28 用户裁工具化:tools/undo_proof.py 对指定套件自动做真撤销(回退指定改动→预期红→还原→预期绿)出回执,替代人工三连;断言配额维持现状不设(用户裁)。 + +**改动面**:工具代码+文档 + +**状态**:待执行 + +#### 闸机影响面选闸——代码域闸件 diff 触发面判定与 SKIP 记账(2026-10-28 登记,来源=会话提及) + +**实况**:证据=术语修正批(三件测试夹具字串)跑了全代码域闸含 02 数值验证;tools/run_all_gates.sh 已有同型先例(11 回放对拍的触面判定+SKIP 记账,PARITY_TOUCH_FACES 机制)。2026-10-28 用户裁推广:按本批 diff 推每闸触发面,未触面闸记 SKIP+理由入回执;触发面映射只写在 run_all_gates.sh 一处;判定保守取向——判不了就跑,不因映射缺漏出假绿。 + +**改动面**:工具代码 + +**状态**:待执行 + #### gdstyle 存量债全量清偿——216 键按病根分波整改(2026-10-26 登记,来源=会话提及,优先级=P0) **实况**:S13 闸实测存量债 216 键/60 文件(2026-10-26,master f3b148ce,gdstyle v0.4.0 默认阈值;基线登记 234 键=实存 216+陈旧 18)。全量清单、波次方案与纪律见清点报告(本条目裁决源)。2026-10-26 用户裁:撤销「一条总账」与「按病根拆多条」两案,并为本条单账(存量债全部清除,紧急优先级);原「gdstyle 基线陈旧键待人工清理」条目并入波 0(基线对账刷新),原「结构审查未立项小件候审」条目并入波 7(卡已写;iter38 施工停卡后经用户裁于 2026-10-26 修订定稿 v1.3,待编排)。**2026-10-26 设计段闭合(逐波裁定全数落定)**:①段组织=逐波开设计段,纯代码波(波 0/2/3/4/5/6/8)写卡前确认零设计改动+按当时 master 重测读数即可写卡;②波 2 各文件内部拆、波 6 视图内部拆私有函数、波 8 config_db 纯代码收敛(三处零设计改动);③波 1 拆件定稿=轴甲两件——battle_orchestrator 拆出 BattleFxHost(效果引擎宿主域)+RewardGrant(奖励授予域),宿主保留状态机/facts 缓冲/订阅/段驱动/行发射基建/呈现投影,attune 族原位保留(迭代 39 副卡语义领地),emit 经 bind 注入 Callable,行数恶化债(c4c69701 挂账)本波还债归绿,残留债「改善非清零」口径预计 8–10 键;定稿落文 architecture.md v1.18(功能层两新件行+「战斗编排域拆件定稿」节);**同日独立审查折入**(grilling+godot-master 双子代理并行、双判回炉补裁级——骨架成立、接线未闭合):阻断 8 条去重 6 组+建议 5 条全数折入 v1.18——跨件单向接线裁死(RewardGrant 持 fx 件引用/fx 兑付走注入 Callable,禁 RefCounted 环)、settlement/ 段类随动(直持三引用封转发壳)、12 随迁键卡内收敛义务(禁波 0 refresh 吸收)、两新件消费依赖列补全+引用束注入、treasure_interact_controller 三触点分家(现值回调双取值面)、_fx_revealing 再入闸归 fx 件(裁定全文见清点报告 §六第 6 条);④全部波不改游戏规则与玩家可见行为,design/gdd/systems/ 零修订预期;波 5 已实查落定纯代码(open_ticket 12 参=单据声明形态)。波 7 的 wr5/wr6 验收载体重裁为写卡段用户裁点(卡 v1.2 在册,写卡补呈报)。裁定全文见清点报告 §六。 From d47a55345be96cf53f6929e20692392237a53f38 Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Wed, 28 Oct 2026 04:00:15 +0800 Subject: [PATCH 5/8] =?UTF-8?q?=E5=B7=A5=E7=A8=8B(=E4=B8=80=E5=8D=A1?= =?UTF-8?q?=E4=B8=80=E6=8F=90=E4=BA=A4=E4=B8=8A=E5=B2=B8=E5=A5=97=E4=BB=B6?= =?UTF-8?q?):=20reshape=5Fcards=20=E9=87=8D=E5=A1=91=E5=B7=A5=E5=85=B7?= =?UTF-8?q?=EF=BC=8B=E5=90=88=E5=9B=9E=E9=97=B8=E6=95=B0=E5=8D=A1=E4=B8=89?= =?UTF-8?q?=E5=88=A4=E6=8D=AE=EF=BC=8B=E5=9B=9E=E6=89=A7=20tree=20?= =?UTF-8?q?=E5=AF=B9=E9=BD=90=EF=BC=8B=E6=89=B9=E6=9C=AB=E9=A1=BA=E5=BA=8F?= =?UTF-8?q?=E5=8A=A0=E9=87=8D=E5=A1=91=E6=AD=A5(process-conventions=20?= =?UTF-8?q?=C2=A71.2=EF=BC=8Biter=20=E6=8A=80=E8=83=BD=E5=90=8C=E8=AF=AD)?= =?UTF-8?q?=E2=80=94=E2=80=94=E5=AE=88=E5=8D=AB=E5=AE=9E=E6=B5=8B=E6=8B=A6?= =?UTF-8?q?=20iter40=20=E5=8D=81=E5=85=AB=E7=AC=94=E6=9C=AA=E5=BD=92?= =?UTF-8?q?=E5=8D=A1=E5=87=BA=E9=87=8D=E5=A1=91=E6=8C=87=E4=BB=A4;?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=E5=AD=A4=E7=AB=8B=E9=AA=8C=E8=AF=81=E4=B8=A4?= =?UTF-8?q?=E5=8D=A1=EF=BC=8B=E6=94=B6=E5=B0=BE=E5=85=A8=E8=B7=AF=E5=BE=84?= =?UTF-8?q?=E9=80=9A=E8=BF=87;=E6=9D=A1=E7=9B=AE=E5=AE=8C=E6=88=90?= =?UTF-8?q?=E5=8D=B3=E5=88=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- design/engineering/process-conventions.md | 2 +- tools/reshape_cards.py | 199 ++++++++++++++++++++++ tools/run_all_gates.sh | 11 +- 待办清单.md | 8 - 4 files changed, 210 insertions(+), 10 deletions(-) create mode 100644 tools/reshape_cards.py diff --git a/design/engineering/process-conventions.md b/design/engineering/process-conventions.md index 06bb9523..84825cfc 100644 --- a/design/engineering/process-conventions.md +++ b/design/engineering/process-conventions.md @@ -10,7 +10,7 @@ 改动按文件性质分两车道(2026-10-26 用户裁,取代 2026-09-19 立的「一切改动走 worktree」铁律;判据与 §3 第 8 条改动域划分同一): 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` 解冲突(必须在跑闸之前,闸跑完再并=那次闸白跑);**重塑**=迭代分支(iter 形)在跑闸前执行 `python tools/reshape_cards.py`,把分支线性史按卡末态快照压成一卡一笔(可选追加一笔纯文档收尾),master 台账由此每卡一行——工具自带「重塑前后树逐字节一致」断言与 backup 分支,不改任何文件内容;非迭代工程小批免重塑;然后按本批改动域跑**一次** `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,非规则强制 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 diff --git a/tools/reshape_cards.py b/tools/reshape_cards.py new file mode 100644 index 00000000..72f609fd --- /dev/null +++ b/tools/reshape_cards.py @@ -0,0 +1,199 @@ +#!/usr/bin/env python3 +"""一卡一提交上岸套件的重塑工具:把迭代分支的线性提交史按卡末态快照压成一卡一笔。 + +分支内自由提交(T 段、修复、文书随做随提),master 台账只收每张卡一笔提交(可选追加 +一笔纯文档收尾)。本工具在批末「并 master 之后、跑闸之前」执行:以每张卡最后一笔提交 +的文件树建新提交、逐卡链在 master 尖上;自带「重塑前后树逐字节一致」断言与 backup +分支,重塑只改账面、不改变任何文件内容。 + +用法: + python tools/reshape_cards.py --branch iter40 \\ + --card 卡1=<该卡最后一笔提交> --card 卡2=<...> [--tail <收尾最后一笔>] [--dry-run] + 可选 --msg 卡1="一行结论"(多条)补提交说明;缺省说明=卡号本身。 + +规则: + 1. master 尖必须是分支祖先(批末先并 master;不满足即拒绝,防快照回退 master 内容)。 + 2. 各卡边界必须是分支第一-parent 链上的提交、按链序递增;最后一笔卡边界或收尾边界 + 必须恰为分支尖——否则存在未归卡提交,拒绝执行并列出。 + 3. 收尾段的改动必须全是文档面(game/ 下非 .md 之外的文件);含代码面先归卡。 + 4. 分支工作区必须干净(脏改动会被快照丢弃,拒绝执行)。 + +非迭代工程小批免重塑(合回闸只对 iter 形分支做数卡校验)。原分支史保留在 +backup/<分支名>-<时间戳> 分支上,可随时回溯。 +""" + +from __future__ import annotations + +import argparse +import re +import subprocess +import sys +import time +from pathlib import Path + +REPO = Path(__file__).resolve().parent.parent + + +def run(args: list[str], check: bool = True, inp: str | None = None) -> str: + r = subprocess.run( + ["git", "-C", str(REPO)] + args, + capture_output=True, text=True, encoding="utf-8", errors="replace", + input=inp, timeout=60, + ) + if check and r.returncode != 0: + sys.stderr.write("git %s 失败:%s\n" % (" ".join(args), (r.stderr or "").strip())) + sys.exit(1) + return (r.stdout or "").strip() + + +def is_doc_face(p: str) -> bool: + # 代码面=game/ 下非 .md 的行为载体;收尾段只许文档面。 + return not (p.startswith("game/") and not p.lower().endswith(".md")) + + +def worktree_of(branch: str) -> str | None: + wl = run(["worktree", "list", "--porcelain"]) + for block in wl.split("\n\n"): + if ("branch refs/heads/%s" % branch) in block: + for line in block.splitlines(): + if line.startswith("worktree "): + return line[9:].strip() + return None + + +def die(msg: str) -> None: + sys.stderr.write("[重塑] 拒绝执行——%s\n" % msg) + sys.exit(1) + + +def main() -> None: + ap = argparse.ArgumentParser(description="按卡末态快照把迭代分支重塑成一卡一笔") + ap.add_argument("--branch", required=True, help="迭代分支名(iter 形)") + ap.add_argument("--card", action="append", required=True, metavar="卡1=<提交>", + help="该卡在分支上的最后一笔提交,按卡序逐卡给出") + ap.add_argument("--tail", metavar="<提交>", default=None, + help="可选收尾段(纯文档面)的最后一笔提交") + ap.add_argument("--msg", action="append", default=[], metavar='卡1="一行结论"', + help="各卡/收尾的提交说明,可多条") + ap.add_argument("--dry-run", action="store_true", help="只打印重塑计划,不动任何引用") + args = ap.parse_args() + + try: + sys.stdout.reconfigure(encoding="utf-8", errors="replace") + sys.stderr.reconfigure(encoding="utf-8", errors="replace") + except Exception: + pass + + branch = args.branch + if not re.fullmatch(r"iter\d+", branch): + die("分支名 %s 不是 iter 形——非迭代批免重塑" % branch) + + master_tip = run(["rev-parse", "master"]) + old_tip = run(["rev-parse", branch]) + rc = subprocess.run(["git", "-C", str(REPO), "merge-base", "--is-ancestor", + master_tip, old_tip], capture_output=True).returncode + if rc != 0: + die("master 尖 %s 不是分支 %s 的祖先——先回分支并 master(git merge master)再重塑," + "否则快照会把 master 新内容回退掉" % (master_tip[:8], branch)) + + wt = worktree_of(branch) + if wt: + st = run(["-C", wt, "-c", "core.quotepath=false", "status", "--porcelain"]) + if st: + die("分支 %s 的工作区(%s)有未提交改动——快照取自提交树,脏改动会被丢弃;" + "先提交或储藏再重塑\n%s" % (branch, wt, st)) + # 分支未检出时引用移动不影响任何工作区,无需检查脏树 + + base = run(["merge-base", "master", branch]) + chain = [x for x in run(["rev-list", "--first-parent", "%s..%s" % (base, branch)]).splitlines() if x] + chain.reverse() # 老 → 新 + if not chain: + die("分支 %s 相对 master 无自有提交,无可重塑" % branch) + idx = {sha: i for i, sha in enumerate(chain)} + + msgs: dict[str, str] = {} + for m in args.msg: + k, _, v = m.partition("=") + msgs[k.strip()] = v.strip() + + cards: list[tuple[str, int]] = [] + for c in args.card: + label, _, sha = c.partition("=") + full = run(["rev-parse", sha.strip()]) + if full not in idx: + die("卡边界 %s(%s)不在分支 %s 相对 master 的第一-parent 链上" % (label, sha, branch)) + cards.append((label.strip(), idx[full])) + cards.sort(key=lambda x: x[1]) + if len({i for _, i in cards}) != len(cards): + die("卡边界重复指向同一笔提交") + + tail_i: int | None = None + if args.tail: + full = run(["rev-parse", args.tail.strip()]) + if full not in idx: + die("收尾边界 %s 不在分支第一-parent 链上" % args.tail) + tail_i = idx[full] + if cards and tail_i <= cards[-1][1]: + die("收尾边界必须排在最后一笔卡边界之后") + + last = tail_i if tail_i is not None else (cards[-1][1] if cards else -1) + if last != len(chain) - 1: + rest = chain[last + 1:] + die("最后一笔边界之后还有 %d 笔未归卡提交——归入所属卡或收尾后重试:\n %s" + % (len(rest), "\n ".join(run(["log", "--no-walk", "--format=%h %s", s]) for s in rest[:8]))) + + # 收尾段只许文档面 + if tail_i is not None: + prev = chain[cards[-1][1]] if cards else base + files = run(["-c", "core.quotepath=false", "diff", "--name-only", prev, chain[tail_i]]).splitlines() + bad = [f for f in files if not is_doc_face(f.strip())] + if bad: + die("收尾段含代码面改动(应归卡):\n " + "\n ".join(bad[:10])) + + # 建新链:master 尖起,逐卡快照 + plan: list[tuple[str, str, str]] = [] # (说明, 边界 sha, 新提交占位) + segs: list[tuple[str, int]] = cards + ([("收尾", tail_i)] if tail_i is not None else []) + parent = master_tip + new_shas: list[str] = [] + n_match = re.fullmatch(r"iter(\d+)", branch) + itn = n_match.group(1) if n_match else branch + for k, (label, i) in enumerate(segs, start=1): + boundary = chain[i] + tree = run(["rev-parse", "%s^{tree}" % boundary]) + if label == "收尾": + subject = "iter%s-收尾" % itn + else: + subject = "iter%s-卡%d" % (itn, k) + if label in msgs: + subject += ":" + msgs[label] + cp = run(["commit-tree", tree, "-p", parent], inp=subject + "\n") + new_shas.append(cp) + parent = cp + plan.append((subject, boundary, cp)) + new_tip = parent + + diff = run(["diff", "--stat", old_tip, new_tip]) + if diff: + die("安全断言失败:重塑前后树不一致(不应发生,请带输出报缺陷)\n%s" % diff) + + print("[重塑] 计划(分支 %s,原尖 %s):" % (branch, old_tip[:8])) + for subject, boundary, cp in plan: + print(" %s ← %s" % (subject, run(["log", "--no-walk", "--format=%h %s", boundary])[:70])) + print("[重塑] 树一致断言通过;backup 分支将指向原尖 %s" % old_tip[:8]) + if args.dry_run: + print("[重塑] --dry-run:未动任何引用。") + return + + backup = "backup/%s-%s" % (branch, time.strftime("%Y%m%d-%H%M%S")) + run(["branch", backup, old_tip]) + if wt: + run(["-C", wt, "reset", "--hard", new_tip]) + else: + run(["update-ref", "refs/heads/%s" % branch, new_tip]) + print("[重塑] 完成:分支 %s → %s(%d 笔卡提交);原史留 %s" + % (branch, new_tip[:8], len(cards), backup)) + print("[重塑] 下一步:按本批改动域跑 tools/run_all_gates.sh,全绿后主工作区合回。") + + +if __name__ == "__main__": + main() diff --git a/tools/run_all_gates.sh b/tools/run_all_gates.sh index 9171e07c..1ace0fc5 100644 --- a/tools/run_all_gates.sh +++ b/tools/run_all_gates.sh @@ -3,7 +3,7 @@ # 闸清单的唯一定义处。文档只写「收口跑本脚本」,禁在文档里另列闸件清单(那正是 iter27 # 漏跑 probe_gate --full 的成因:清单散落三份文档、彼此不一致、且无机器校验)。 # -# 跑完写回执 game/tests/.gates/receipt.json(commit + 各闸退出码 + 存量基线比对 + 时间戳); +# 跑完写回执 game/tests/.gates/receipt.json(commit+内容树哈希+各闸退出码+存量基线比对+时间戳); # .zcode/hooks/worktree-guard.sh 在主工作区 git merge 时校验本回执,缺或红即拦截。 # # 本脚本是合回前的终局动作:跑之前先冻结(代码与断言不再改)并把 master 并入本分支;每批只跑 @@ -304,6 +304,14 @@ def head(): except Exception: return "unknown" +def tree_hash(): + # 内容树哈希:合回闸按树对齐判定「合回内容=跑闸内容」(重塑/合并会改写 commit 号) + try: + return subprocess.run(["git", "-C", repo, "rev-parse", "HEAD^{tree}"], + capture_output=True, text=True, timeout=5).stdout.strip() + except Exception: + return "unknown" + def fingerprint(exit_code, log): """红项指纹:只取 exit!=0 时的 FAIL 关键行集合,忽略时序/耗时噪声。 绿闸一律记 'green'——绿闸日志里的「零红」「全绿」等字样不是红项,采进来只会造噪声。""" @@ -370,6 +378,7 @@ with open(receipt_path, "w", encoding="utf-8") as f: json.dump({ "commit": head(), "commit_short": head()[:8], + "tree": tree_hash(), "timestamp": subprocess.run(["date", "-Iseconds"], capture_output=True, text=True).stdout.strip(), "quick_mode": quick == "1", "domain": domain, diff --git a/待办清单.md b/待办清单.md index 53d7a51a..6f1797e6 100644 --- a/待办清单.md +++ b/待办清单.md @@ -2,14 +2,6 @@ 本清单是流水线的唯一登记处:要做的事先在此登记一条,随流水线段推进改状态,做完整条删除;各系统文档的「待办」节只留一行指路存根,不再自建条目。条目格式(标题=登记元数据含可选优先级,正文=实况/缺口/改动面/状态四属性)与状态机以 [doc-conventions §12.1](design/engineering/doc-conventions.md) 为权威。 -#### 一卡一提交上岸套件——分支重塑工具+合回闸数卡判据+回执树哈希对齐(2026-10-28 登记,来源=会话提及) - -**实况**:证据=迭代 39/40 两个实现会话 44 笔提交中非实装 27 笔(git log 实测),碎片提交与收口文书直接上岸 master 是最大宗。2026-10-28 用户裁落地形态=重塑方案:分支内自由提交、批末 `tools/reshape_cards.py` 按卡末态快照重塑为一卡一笔(可选一笔文档收尾)、合回闸校验卡数与提交格式、回执以内容树哈希对齐(重塑与合并改写 commit 号后仍可判定「合回内容=跑闸内容」)。批末顺序改为「并 master → 重塑 → 跑闸 → 合回」。落点:tools/reshape_cards.py(新建)+tools/run_all_gates.sh(回执加 tree 字段)+.zcode/hooks/worktree-guard.sh(数卡/无杂笔/格式三判据+树对齐)+design/engineering/process-conventions.md §1.2(批末顺序与上岸形态)+.zcode/skills/iter/SKILL.md(同语)。工程改动(工具代码+文档),用户已发令(2026-10-28 四批全做裁决)。 - -**改动面**:工具代码+文档 - -**状态**:待执行 - #### 撤销证明工具化——undo_proof 自动红绿回执+变异禁令落 test-conventions(2026-10-28 登记,来源=会话提及) **实况**:证据=迭代 40 卡① 十例取证人工三连(绿→真撤销复红→还原复绿)且撞 2026-10-24 变异禁令——禁令原文只在 req-card 技能内,test-conventions 指针空挂(iter40 反哺点 B3 在册)。2026-10-28 用户裁工具化:tools/undo_proof.py 对指定套件自动做真撤销(回退指定改动→预期红→还原→预期绿)出回执,替代人工三连;断言配额维持现状不设(用户裁)。 From 219af09a04dba35fa50b207a85c314a6829c55f0 Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Wed, 28 Oct 2026 04:04:01 +0800 Subject: [PATCH 6/8] =?UTF-8?q?=E5=B7=A5=E7=A8=8B(=E6=92=A4=E9=94=80?= =?UTF-8?q?=E8=AF=81=E6=98=8E=E5=B7=A5=E5=85=B7=E5=8C=96):=20undo=5Fproof?= =?UTF-8?q?=20=E4=B8=80=E6=9D=A1=E5=91=BD=E4=BB=A4=E5=87=BA=E3=80=8C?= =?UTF-8?q?=E6=94=B9=E5=89=8D=E7=BB=BF=E2=86=92=E7=9C=9F=E6=92=A4=E9=94=80?= =?UTF-8?q?=E5=A4=8D=E7=BA=A2=E2=86=92=E8=BF=98=E5=8E=9F=E5=A4=8D=E7=BB=BF?= =?UTF-8?q?=E3=80=8D=E4=B8=89=E8=BF=9E=E5=9B=9E=E6=89=A7=E5=B9=B6=E8=87=AA?= =?UTF-8?q?=E8=BF=98=E5=8E=9F(=E6=94=AF=E6=8C=81=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E5=9E=8B=E6=92=A4=E9=94=80=E4=B8=8E--repo=20?= =?UTF-8?q?=E8=B7=A8=E5=8C=BA)=EF=BC=8B=E5=8F=98=E5=BC=82=E7=A6=81?= =?UTF-8?q?=E4=BB=A4=E6=94=B6=E7=BC=96=E8=BF=9B=20test-conventions=20?= =?UTF-8?q?=C2=A78.4(=E6=AD=A4=E5=89=8D=E5=8F=AA=E6=B4=BB=E5=9C=A8=20req-c?= =?UTF-8?q?ard=20=E6=8A=80=E8=83=BD,=E6=89=A7=E8=A1=8C=E4=BE=A7=E7=A9=BA?= =?UTF-8?q?=E6=8C=82)=E2=80=94=E2=80=94=E5=AD=A4=E7=AB=8B=E9=AA=8C?= =?UTF-8?q?=E8=AF=81=E9=80=9A=E8=BF=87/=E6=81=92=E7=BB=BF=E7=BC=BA?= =?UTF-8?q?=E6=BC=8F=E5=88=A4=E7=BA=A2=E8=B7=AF=E5=BE=84=E9=80=9A=E8=BF=87?= =?UTF-8?q?;=E6=96=AD=E8=A8=80=E9=85=8D=E9=A2=9D=E7=BB=B4=E6=8C=81?= =?UTF-8?q?=E7=8E=B0=E7=8A=B6(=E7=94=A8=E6=88=B7=E8=A3=81);=E6=9D=A1?= =?UTF-8?q?=E7=9B=AE=E5=AE=8C=E6=88=90=E5=8D=B3=E5=88=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- design/engineering/test-conventions.md | 1 + tools/undo_proof.py | 159 +++++++++++++++++++++++++ 待办清单.md | 8 -- 3 files changed, 160 insertions(+), 8 deletions(-) create mode 100644 tools/undo_proof.py diff --git a/design/engineering/test-conventions.md b/design/engineering/test-conventions.md index d8a3f8c3..4cf40700 100644 --- a/design/engineering/test-conventions.md +++ b/design/engineering/test-conventions.md @@ -105,6 +105,7 @@ - **撤销后仍然绿**:说明断言没钉在这个改动上,判缺漏;不得用「代码能跑通」代替。 - **判据绑可观测提交面**:端到端断言的判据要绑「被测改动特有的可观测提交面」(信号发射、事件、入参、绘制记录),不绑多层防御之后的终态——否则改动被下游兜底吸收时终态不变、断言假绿。例:探针「雾不扩可点域」首版只看屏/选路态终态,命中面放宽后断言仍绿;改绑 `route_chosen` 发射面后即红。 - 每张任务卡至少一条撤销证明记录;持久化、生命周期、存档、跨战共享这类接线逐行做。 +- **合法手段只有真撤销(2026-10-24 用户立:全项目禁做变异类测试)**——改断言极性、置空库存/状态、改名键、删调用行等「人为制造红」都属伪造证据,一律禁止;证明只认「回退本批真实改动 → 断言红 → 还原复绿」。自动化载体=`tools/undo_proof.py`(给最窄测试命令与撤销对象,自动出三连红绿回执并自还原),人工三连跑由此免做。 ### 8.5 禁止削弱测试 diff --git a/tools/undo_proof.py b/tools/undo_proof.py new file mode 100644 index 00000000..071fd430 --- /dev/null +++ b/tools/undo_proof.py @@ -0,0 +1,159 @@ +#!/usr/bin/env python3 +"""撤销证明自动化工具:一条命令完成「改前绿 → 真撤销复红 → 还原复绿」三连并出回执。 + +撤销证明的判据(test-conventions §8.4):只撤销该改动、跑能覆盖它的最窄测试、对应断言 +必须变红、还原后复绿;撤销后仍绿=断言没钉在改动上,判缺漏。本工具把这三连机械化—— +合法性属于「真撤销」(回退本批真实改动),全项目禁做变异类测试(改断言极性、置空状态、 +改名键、删调用行等人为制造红=伪造证据)。 + +用法: + python tools/undo_proof.py --cmd "<最窄测试命令>" --revert-commit <该批提交> + python tools/undo_proof.py --cmd "<最窄测试命令>" --revert-files --base + 可选 --out 落回执;--timeout <秒> 限制单次测试命令时长(缺省 3600)。 + + --revert-commit:按该笔提交触过的文件集回退到其父提交版本(改动须已提交在分支上)。 + --revert-files+--base:把指定文件回退到 (默认 HEAD)版本——适合改动尚未单独 + 成笔、混在当前尖里的场景。 + 测试命令在仓库根以 shell 执行;单套件直跑形态见 game/tests/README.md(测试工具手册)。 + +流程与退出码: + ① 改前基线:测试命令必须绿(红=改前就红,无证明意义,中止)。 + ② 真撤销:指定文件回退到基准版本 → 重跑命令,必须红(红=断言钉在该改动上)。 + ③ 还原:文件恢复到 HEAD → 重跑命令,必须绿。 + ④ 任何一步异常都先还原再退出;三步全过=退出码 0 并打印回执,否则退出码 1。 +""" + +from __future__ import annotations + +import argparse +import json +import subprocess +import sys +from pathlib import Path + +REPO_DEFAULT = Path(__file__).resolve().parent.parent +REPO = REPO_DEFAULT + + +def run(args: list[str], check: bool = True) -> str: + r = subprocess.run(["git", "-C", str(REPO)] + args, + capture_output=True, text=True, encoding="utf-8", errors="replace", + timeout=60) + if check and r.returncode != 0: + sys.stderr.write("git %s 失败:%s\n" % (" ".join(args), (r.stderr or "").strip())) + sys.exit(1) + return (r.stdout or "").strip() + + +def run_cmd(cmd: str, timeout: int) -> int: + r = subprocess.run(cmd, shell=True, cwd=str(REPO), capture_output=True, + text=True, encoding="utf-8", errors="replace", timeout=timeout) + if r.returncode != 0: + tail = "\n".join((r.stdout or "").splitlines()[-15:] or ["(无输出)"]) + sys.stderr.write(" | 测试命令失败(exit=%d),输出尾部:\n%s\n" % (r.returncode, tail)) + return r.returncode + + +def _in_rev(rev: str, f: str) -> bool: + r = subprocess.run(["git", "-C", str(REPO), "cat-file", "-e", "%s:%s" % (rev, f)], + capture_output=True, timeout=30) + return r.returncode == 0 + + +def apply_rev(rev: str, files: list[str]) -> None: + # 逐文件:基准版本存在 → 恢复到该版本;基准没有(该批新增的文件)→ 从工作区删除。 + for f in files: + if _in_rev(rev, f): + run(["checkout", rev, "--", f]) + else: + subprocess.run(["git", "-C", str(REPO), "rm", "-q", "-f", "--", f], + capture_output=True, text=True, timeout=30) + + +def main() -> None: + ap = argparse.ArgumentParser(description="自动三连出撤销证明红绿回执") + ap.add_argument("--cmd", required=True, help="最窄测试命令(shell 形,仓库根执行)") + ap.add_argument("--revert-commit", default=None, help="该批提交:按其触过的文件集回退到父版本") + ap.add_argument("--revert-files", default=None, help="逗号分隔的文件清单(与 --base 配用)") + ap.add_argument("--base", default="HEAD", help="回退基准版本(默认 HEAD)") + ap.add_argument("--timeout", type=int, default=3600, help="单次测试命令时长上限(秒)") + ap.add_argument("--repo", default=None, help="仓库/worktree 路径(缺省=本脚本所在仓库)") + ap.add_argument("--out", default=None, help="回执 JSON 落盘路径") + args = ap.parse_args() + + global REPO + REPO = Path(args.repo).resolve() if args.repo else REPO_DEFAULT + + try: + sys.stdout.reconfigure(encoding="utf-8", errors="replace") + sys.stderr.reconfigure(encoding="utf-8", errors="replace") + except Exception: + pass + + if bool(args.revert_commit) == bool(args.revert_files): + ap.error("--revert-commit 与 --revert-files 二选一") + + if args.revert_commit: + files = [x.strip() for x in run( + ["-c", "core.quotepath=false", "show", "--name-only", "--format=", + args.revert_commit]).splitlines() if x.strip()] + base = "%s^" % args.revert_commit + if not files: + sys.stderr.write("[撤销证明] 提交 %s 未触碰任何文件,无可撤销\n" % args.revert_commit) + sys.exit(2) + else: + files = [x.strip() for x in args.revert_files.split(",") if x.strip()] + base = args.base + + dirty = run(["-c", "core.quotepath=false", "status", "--porcelain", "--"] + files) + if dirty: + sys.stderr.write("[撤销证明] 指定文件带未提交改动,撤销与还原的基准不清,先提交:\n%s\n" % dirty) + sys.exit(2) + + steps: list[dict[str, object]] = [] + result = "pass" + + def step(name: str, expect: str, code: int) -> bool: + ok = (code == 0) if expect == "绿" else (code != 0) + steps.append({"step": name, "expect": expect, "exit": code, "ok": ok}) + print("[撤销证明] %s:期望%s,实测 exit=%d → %s" + % (name, expect, code, "符合" if ok else "不符合")) + return ok + + print("[撤销证明] 对象文件 %d 个;基准 %s;命令:%s" % (len(files), base, args.cmd)) + try: + if not step("① 改前基线", "绿", run_cmd(args.cmd, args.timeout)): + result = "fail(改前就红——先修绿再做撤销证明)" + else: + apply_rev(base, files) + try: + if not step("② 真撤销复红", "红", run_cmd(args.cmd, args.timeout)): + result = "fail(撤销后仍绿=断言没钉在该改动上,判缺漏)" + else: + apply_rev("HEAD", files) + if not step("③ 还原复绿", "绿", run_cmd(args.cmd, args.timeout)): + result = "fail(还原后未复绿——还原面有残留,查工作区)" + finally: + apply_rev("HEAD", files) + except subprocess.TimeoutExpired: + result = "fail(测试命令超时)" + finally: + # 兜底还原:无论走到哪一步,退出前把对象文件恢复到 HEAD 态 + apply_rev("HEAD", files) + + residue = run(["-c", "core.quotepath=false", "status", "--porcelain", "--"] + files) + if residue: + result = "fail(退出前工作区仍有对象文件残留改动,手工核对)" + + print("─" * 56) + print("[撤销证明] 结论:%s" % result) + if args.out: + with open(args.out, "w", encoding="utf-8") as f: + json.dump({"cmd": args.cmd, "base": base, "files": files, + "steps": steps, "result": result}, f, ensure_ascii=False, indent=1) + print("[撤销证明] 回执已落 %s" % args.out) + sys.exit(0 if result == "pass" else 1) + + +if __name__ == "__main__": + main() diff --git a/待办清单.md b/待办清单.md index 6f1797e6..cca3a59c 100644 --- a/待办清单.md +++ b/待办清单.md @@ -2,14 +2,6 @@ 本清单是流水线的唯一登记处:要做的事先在此登记一条,随流水线段推进改状态,做完整条删除;各系统文档的「待办」节只留一行指路存根,不再自建条目。条目格式(标题=登记元数据含可选优先级,正文=实况/缺口/改动面/状态四属性)与状态机以 [doc-conventions §12.1](design/engineering/doc-conventions.md) 为权威。 -#### 撤销证明工具化——undo_proof 自动红绿回执+变异禁令落 test-conventions(2026-10-28 登记,来源=会话提及) - -**实况**:证据=迭代 40 卡① 十例取证人工三连(绿→真撤销复红→还原复绿)且撞 2026-10-24 变异禁令——禁令原文只在 req-card 技能内,test-conventions 指针空挂(iter40 反哺点 B3 在册)。2026-10-28 用户裁工具化:tools/undo_proof.py 对指定套件自动做真撤销(回退指定改动→预期红→还原→预期绿)出回执,替代人工三连;断言配额维持现状不设(用户裁)。 - -**改动面**:工具代码+文档 - -**状态**:待执行 - #### 闸机影响面选闸——代码域闸件 diff 触发面判定与 SKIP 记账(2026-10-28 登记,来源=会话提及) **实况**:证据=术语修正批(三件测试夹具字串)跑了全代码域闸含 02 数值验证;tools/run_all_gates.sh 已有同型先例(11 回放对拍的触面判定+SKIP 记账,PARITY_TOUCH_FACES 机制)。2026-10-28 用户裁推广:按本批 diff 推每闸触发面,未触面闸记 SKIP+理由入回执;触发面映射只写在 run_all_gates.sh 一处;判定保守取向——判不了就跑,不因映射缺漏出假绿。 From 667a6060f8be4222239c7272ce0de7e215e06647 Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Wed, 28 Oct 2026 04:06:55 +0800 Subject: [PATCH 7/8] =?UTF-8?q?=E5=B7=A5=E7=A8=8B(=E5=BD=B1=E5=93=8D?= =?UTF-8?q?=E9=9D=A2=E9=80=89=E9=97=B8):=20=E4=BB=A3=E7=A0=81=E5=9F=9F?= =?UTF-8?q?=E9=97=B8=E5=A3=B3=E6=8C=89=20diff=20=E6=8E=A8=E8=A7=A6?= =?UTF-8?q?=E5=8F=91=E9=9D=A2=E2=80=94=E2=80=94game/=20=E6=94=B9=E5=8A=A8?= =?UTF-8?q?=E5=85=A8=E5=9C=A8=E6=B5=8B=E9=87=8F=E9=9D=A2(tests/sim)?= =?UTF-8?q?=E6=97=B6=2000=20=E5=86=92=E7=83=9F/03/04=20=E6=8E=A2=E9=92=88/?= =?UTF-8?q?05=20=E6=B8=B2=E6=9F=93=E9=97=B8=E8=AE=B0=20SKIP,01/02/09=20?= =?UTF-8?q?=E6=81=92=E8=B7=91(=E6=B5=8B=E9=87=8F=E9=9D=A2=E6=94=B9?= =?UTF-8?q?=E5=8A=A8=E7=9A=84=E9=AA=8C=E8=AF=81=E8=BD=BD=E4=BD=93),11=20?= =?UTF-8?q?=E5=AF=B9=E6=8B=8D=E8=A7=A6=E9=9D=A2=E5=88=A4=E5=AE=9A=E6=B2=BF?= =?UTF-8?q?=E7=94=A8=E6=97=A2=E6=9C=89=E6=9C=BA=E5=88=B6=E2=80=94=E2=80=94?= =?UTF-8?q?=E7=9C=9F=E5=80=BC=E8=A1=A8=E4=BA=94=E4=BE=8B=E9=AA=8C=E8=AF=81?= =?UTF-8?q?(=E5=90=AB=E6=B7=B7=E8=A1=8C=E4=B8=BA=E9=9D=A2/SIM=20=E5=86=92?= =?UTF-8?q?=E5=90=8D),=E6=9C=AC=E6=9C=BA=20grep=20-q=20-v=20=E9=80=80?= =?UTF-8?q?=E5=87=BA=E7=A0=81=E5=8F=8D=E5=B8=B8=E6=94=B9=E7=94=A8=E5=8F=8D?= =?UTF-8?q?=E9=80=89=E5=88=A4=E7=A9=BA=E5=8F=AF=E7=A7=BB=E6=A4=8D=E5=86=99?= =?UTF-8?q?=E6=B3=95;=E6=98=A0=E5=B0=84=E5=8D=95=E7=82=B9=E5=9C=A8?= =?UTF-8?q?=E9=97=B8=E5=A3=B3,SKIP=20=E4=B8=8E=20--quick=20=E5=90=8C?= =?UTF-8?q?=E6=9E=84=E5=9B=9E=E6=89=A7=E8=AE=B0=20not=5Frun;=E6=9D=A1?= =?UTF-8?q?=E7=9B=AE=E5=AE=8C=E6=88=90=E5=8D=B3=E5=88=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tools/run_all_gates.sh | 44 +++++++++++++++++++++++++++++++++++------- 待办清单.md | 8 -------- 2 files changed, 37 insertions(+), 15 deletions(-) diff --git a/tools/run_all_gates.sh b/tools/run_all_gates.sh index 1ace0fc5..4f7597f1 100644 --- a/tools/run_all_gates.sh +++ b/tools/run_all_gates.sh @@ -14,6 +14,8 @@ # --code 只跑代码域闸(00 冒烟测试/01 系统测试/02 单元与集成测试/03 探针 --click/ # 04 探针 --full/05 渲染闸/09 静态扫描/11 回放确定性对拍+10 熵增闸) # ——本批改动含 game/ 下非 .md 的行为载体(代码/测试脚本/探针/场景/数据表/素材)时用。 +# 影响面选闸:本批 game/ 改动全在测量面(game/tests、game/sim)时,00/03/04/05 记 +# SKIP(测量面不触开机路径/场景树/渲染输出;映射定义在脚本中部,判定保守)。 # --docs 只跑文档域闸(06 gdd_lint/07 doc_link/08 doc_term/08c 流水线闸/08d 引用可核 # +10 熵增闸) # ——本批改动只有文档件时用:game/ 内的 .md(README、生成清单)、design/、根规则文件、 @@ -152,6 +154,23 @@ fi declare -a NAMES=() EXITS=() LOGS=() FAILED=0 +# ── 影响面选闸(2026-10-28 用户裁):本批 diff 相对 master 分叉点的 game/ 改动若全部落在 +# 测量面(game/tests/**、game/sim/**——测试与模拟驱动件,不触开机路径、场景树与渲染 +# 输出),则 00 冒烟/03/04 探针/05 渲染闸记 SKIP。判定保守取向:diff 含任一行为面 +# 文件即全部实跑;01/02/09 恒跑(测试宿主与静态扫描正是测量面改动的验证载体)。 +# 触发面映射只在本脚本一处定义;SKIP 与 --quick 同构,回执记 not_run,不作闸况变化。 +NONBEHAVIOR=0 +if in_domain code; then + _base="$(git -C "$REPO" merge-base master HEAD 2>/dev/null || true)" + _gfiles="$(git -C "$REPO" -c core.quotepath=false diff --name-only "${_base:-HEAD}" HEAD -- game/ 2>/dev/null || true)" + # 反选(测量面之外的行)非空=含行为面。不用 grep -q:本机 git-bash 的 -q -v 组合退出码反常。 + _behavior="$(echo "$_gfiles" | grep -vE '^(game/tests/|game/sim/)' || true)" + if [ -n "$_gfiles" ] && [ -z "$_behavior" ]; then + NONBEHAVIOR=1 + echo "[gates] 影响面选闸:game/ 改动全在测量面(tests/sim)——00 冒烟/03/04 探针/05 渲染闸 → SKIP" + fi +fi + echo "闸机跑批 · 域=$DOMAIN · 仓库 $REPO" echo "起跑 commit: $(git -C "$REPO" rev-parse --short HEAD 2>/dev/null || echo '非git目录')" echo "python: $PY" @@ -197,21 +216,32 @@ gate_unit() { # run_all 是 --script 模式,退出码为唯一判据 # ══ 代码域(0–5)══ if in_domain code; then # ── 0. 冒烟测试(极小启动场景:真实开机路径存活检查,零逻辑判定,秒级)── -gate "00 冒烟测试" "smoke_boot.log" "$PY" "$REPO/tools/run_smoke.py" +if [ $NONBEHAVIOR -eq 1 ]; then + NAMES+=("00 冒烟测试"); EXITS+=("SKIP"); LOGS+=("") + echo ""; echo "[gates] 00 冒烟测试 → SKIP(影响面选闸:测量面改动不触开机路径)" +else + gate "00 冒烟测试" "smoke_boot.log" "$PY" "$REPO/tools/run_smoke.py" +fi # ── 1. 系统测试(真实玩法路径断言面;宿主=game/tests/system_test.gd,缺省 --script 由 run_system_test.py 持有)── gate "01 系统测试" "system_test.log" "$PY" "$REPO/tools/run_system_test.py" # ── 2. 单元与集成测试 ── gate_unit # ── 3. 场景探针 · 行为走查段 ── -gate "03 探针 --click" "probe_click.log" bash "$REPO/tools/probe_gate.sh" "$GAME" --click # ── 4. 场景探针 · 断言段(iter27 漏跑的就是这一段)── -gate "04 探针 --full" "probe_full.log" bash "$REPO/tools/probe_gate.sh" "$GAME" --full -# ── 5. 渲染面(窗口化,依赖 PIL)── -if [ $QUICK -eq 0 ]; then - gate "05 渲染闸" "render.log" bash "$REPO/tools/render_gate.sh" "$GAME" +if [ $NONBEHAVIOR -eq 1 ]; then + NAMES+=("03 探针 --click"); EXITS+=("SKIP"); LOGS+=("") + NAMES+=("04 探针 --full"); EXITS+=("SKIP"); LOGS+=("") + echo ""; echo "[gates] 03/04 探针 → SKIP(影响面选闸:测量面改动不触场景树与探针断言面)" else + gate "03 探针 --click" "probe_click.log" bash "$REPO/tools/probe_gate.sh" "$GAME" --click + gate "04 探针 --full" "probe_full.log" bash "$REPO/tools/probe_gate.sh" "$GAME" --full +fi +# ── 5. 渲染面(窗口化,依赖 PIL)── +if [ $QUICK -eq 1 ] || [ $NONBEHAVIOR -eq 1 ]; then NAMES+=("05 渲染闸"); EXITS+=("SKIP"); LOGS+=("") - echo ""; echo "[gates] 05 渲染闸 → SKIP(--quick)" + echo ""; echo "[gates] 05 渲染闸 → SKIP(--quick 或 影响面选闸:测量面改动不触渲染输出)" +else + gate "05 渲染闸" "render.log" bash "$REPO/tools/render_gate.sh" "$GAME" fi fi # ══ 文档域(6–8c)══ diff --git a/待办清单.md b/待办清单.md index cca3a59c..fc8c0dd8 100644 --- a/待办清单.md +++ b/待办清单.md @@ -2,14 +2,6 @@ 本清单是流水线的唯一登记处:要做的事先在此登记一条,随流水线段推进改状态,做完整条删除;各系统文档的「待办」节只留一行指路存根,不再自建条目。条目格式(标题=登记元数据含可选优先级,正文=实况/缺口/改动面/状态四属性)与状态机以 [doc-conventions §12.1](design/engineering/doc-conventions.md) 为权威。 -#### 闸机影响面选闸——代码域闸件 diff 触发面判定与 SKIP 记账(2026-10-28 登记,来源=会话提及) - -**实况**:证据=术语修正批(三件测试夹具字串)跑了全代码域闸含 02 数值验证;tools/run_all_gates.sh 已有同型先例(11 回放对拍的触面判定+SKIP 记账,PARITY_TOUCH_FACES 机制)。2026-10-28 用户裁推广:按本批 diff 推每闸触发面,未触面闸记 SKIP+理由入回执;触发面映射只写在 run_all_gates.sh 一处;判定保守取向——判不了就跑,不因映射缺漏出假绿。 - -**改动面**:工具代码 - -**状态**:待执行 - #### gdstyle 存量债全量清偿——216 键按病根分波整改(2026-10-26 登记,来源=会话提及,优先级=P0) **实况**:S13 闸实测存量债 216 键/60 文件(2026-10-26,master f3b148ce,gdstyle v0.4.0 默认阈值;基线登记 234 键=实存 216+陈旧 18)。全量清单、波次方案与纪律见清点报告(本条目裁决源)。2026-10-26 用户裁:撤销「一条总账」与「按病根拆多条」两案,并为本条单账(存量债全部清除,紧急优先级);原「gdstyle 基线陈旧键待人工清理」条目并入波 0(基线对账刷新),原「结构审查未立项小件候审」条目并入波 7(卡已写;iter38 施工停卡后经用户裁于 2026-10-26 修订定稿 v1.3,待编排)。**2026-10-26 设计段闭合(逐波裁定全数落定)**:①段组织=逐波开设计段,纯代码波(波 0/2/3/4/5/6/8)写卡前确认零设计改动+按当时 master 重测读数即可写卡;②波 2 各文件内部拆、波 6 视图内部拆私有函数、波 8 config_db 纯代码收敛(三处零设计改动);③波 1 拆件定稿=轴甲两件——battle_orchestrator 拆出 BattleFxHost(效果引擎宿主域)+RewardGrant(奖励授予域),宿主保留状态机/facts 缓冲/订阅/段驱动/行发射基建/呈现投影,attune 族原位保留(迭代 39 副卡语义领地),emit 经 bind 注入 Callable,行数恶化债(c4c69701 挂账)本波还债归绿,残留债「改善非清零」口径预计 8–10 键;定稿落文 architecture.md v1.18(功能层两新件行+「战斗编排域拆件定稿」节);**同日独立审查折入**(grilling+godot-master 双子代理并行、双判回炉补裁级——骨架成立、接线未闭合):阻断 8 条去重 6 组+建议 5 条全数折入 v1.18——跨件单向接线裁死(RewardGrant 持 fx 件引用/fx 兑付走注入 Callable,禁 RefCounted 环)、settlement/ 段类随动(直持三引用封转发壳)、12 随迁键卡内收敛义务(禁波 0 refresh 吸收)、两新件消费依赖列补全+引用束注入、treasure_interact_controller 三触点分家(现值回调双取值面)、_fx_revealing 再入闸归 fx 件(裁定全文见清点报告 §六第 6 条);④全部波不改游戏规则与玩家可见行为,design/gdd/systems/ 零修订预期;波 5 已实查落定纯代码(open_ticket 12 参=单据声明形态)。波 7 的 wr5/wr6 验收载体重裁为写卡段用户裁点(卡 v1.2 在册,写卡补呈报)。裁定全文见清点报告 §六。 From 21cd0f2e050753d8992dac58c6d431a095ef0aa1 Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Wed, 28 Oct 2026 04:14:35 +0800 Subject: [PATCH 8/8] =?UTF-8?q?=E5=B7=A5=E7=A8=8B(=E6=8C=82=E6=AD=BB?= =?UTF-8?q?=E9=98=B2=E6=8A=A4=C2=B7probe=5Fgate=E4=BB=B6):=20=E5=8D=81?= =?UTF-8?q?=E5=A4=84=E5=A5=97=E4=BB=B6=E6=8D=A2=20run=5Fsuite=20=E6=95=B4?= =?UTF-8?q?=E6=A0=91=E5=9B=9E=E6=94=B6=E2=80=94=E2=80=94=E5=90=AF=E5=8A=A8?= =?UTF-8?q?=E8=AE=B0=20WINPID,=E8=B6=85=E6=97=B6=E5=85=88=20taskkill=20/T?= =?UTF-8?q?=20=E6=9D=80=E5=85=A8=E6=A0=91=E5=86=8D=20kill=20-9=20=E6=94=B6?= =?UTF-8?q?=20MSYS=20=E4=BE=A7(=E9=A1=BA=E5=BA=8F=E9=A2=A0=E5=80=92?= =?UTF-8?q?=E4=BC=9A=E8=AE=A9=20/T=20=E8=B5=B0=E4=B8=8D=E5=88=B0=E5=B7=B2?= =?UTF-8?q?=E6=AD=BB=E7=88=B6=E8=BF=9B=E7=A8=8B,=E5=8D=B3=E5=8E=9F?= =?UTF-8?q?=E5=AD=A4=E5=84=BF=E6=B3=84=E6=BC=8F=E6=A0=B9=E6=BA=90),?= =?UTF-8?q?=E6=A0=87=E5=BF=97=E6=96=87=E4=BB=B6=E5=88=A4=E8=B6=85=E6=97=B6?= =?UTF-8?q?(taskkill=20=E5=BC=BA=E6=9D=80=E5=90=8E=20MSYS=20wait=20?= =?UTF-8?q?=E6=8A=A5=200)=E2=80=94=E2=80=94=E9=BB=98=E8=AE=A4=E4=BA=94?= =?UTF-8?q?=E6=AE=B5=E5=AE=9E=E8=B7=91=E5=85=A8=E7=BB=BF=E6=AE=8B=E7=95=99?= =?UTF-8?q?0,=E8=B6=85=E6=97=B6/=E6=AD=A3=E5=B8=B8=E5=8F=8C=E8=B7=AF?= =?UTF-8?q?=E5=BE=84=E6=9C=BA=E5=88=B6=E9=AA=8C=E8=AF=81=E9=80=9A=E8=BF=87?= =?UTF-8?q?;run=5Fall.gd=20=E9=98=BB=E5=A1=9E=E9=9D=A2=E8=B6=85=E6=97=B6?= =?UTF-8?q?=E5=AE=88=E6=8A=A4=E5=B1=9E=E4=BB=A3=E7=A0=81=E9=9D=A2=E7=95=99?= =?UTF-8?q?=E5=BE=85=20worktree=20=E6=89=B9,=E6=9D=A1=E7=9B=AE=E4=BF=9D?= =?UTF-8?q?=E6=8C=81=E5=BE=85=E6=89=A7=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tools/probe_gate.sh | 72 ++++++++++++++++++++++++++++++++------------- 待办清单.md | 2 +- 2 files changed, 52 insertions(+), 22 deletions(-) diff --git a/tools/probe_gate.sh b/tools/probe_gate.sh index 1098059d..839caa7d 100644 --- a/tools/probe_gate.sh +++ b/tools/probe_gate.sh @@ -1,6 +1,7 @@ #!/usr/bin/env bash # 探针闸正主——headless 安全段固化,禁再手拼同款 Godot 命令。 -# 三闸:①每段 timeout 守卫(挂死=FAIL 不静默)②段前预清+段后清本 game 目录孤儿 Godot +# 三闸:①每段 timeout 守卫+超时整树回收(挂死=FAIL 不静默;taskkill /T 连引擎子进程一起杀, +# 不再只杀 wrapper 留孤儿)②段前预清+段后清本 game 目录孤儿 Godot # (按命令行精确匹配本 game 路径,不误杀其他 worktree 会话的合法进程)③末尾残留清点=批末闸(必须 0)。 # 段:ui_input_probe=默认必跑;ui_struct_probe=默认必跑( # 全 UI 屏结构断言:按钮互压/等宽/出屏/零尺寸/标题屏双列);ui_contrast_probe=默认必跑( @@ -75,6 +76,35 @@ kill_orphans() { local p; for p in $(orphans); do taskkill //F //PID "$p" >/dev/null 2>&1; done } +# 每段跑批统一走本函数:后台启动记 MSYS PID→WINPID,守卫进程到点先 taskkill /T 整树回收 +# (顺序要紧:先杀 wrapper 再清树会让 /T 走不到已死的父进程,引擎子进程照样泄漏—— +# 2026-10-25 在册挂死根源),后 kill -9 收 MSYS 侧。正常退出不动进程树;段后 kill_orphans 照跑兜底。 +run_suite() { # run_suite <日志> <场景 res:// 路径> + local t="$1" log="$2" scene="$3" + local kf="$TMP/killed.$RANDOM" + rm -f "$kf" + "$G" --headless --path "$GAME_WIN" "$scene" >"$log" 2>&1 & + local pid=$! + local wpid="" + wpid="$(ps -W 2>/dev/null | awk -v p="$pid" '$1==p{print $4; exit}')" + ( sleep "$t" 2>/dev/null + touch "$kf" + [ -n "$wpid" ] && taskkill //F //T //PID "$wpid" >/dev/null 2>&1 + kill -9 "$pid" 2>/dev/null + true ) & + local guard=$! + wait "$pid"; local st=$? + kill "$guard" 2>/dev/null; wait "$guard" 2>/dev/null + # taskkill 强杀后 MSYS wait 可能报 0——超时判定以守卫落下的标志文件为准(与 124 同判)。 + if [ "$st" -ge 128 ] || [ -f "$kf" ]; then + st=124 + echo "[probe_gate] 套件超时(${t}s),已整树回收(PID=$pid/WINPID=${wpid:-未取到——靠孤儿清杀兜底})" + fi + rm -f "$kf" + kill_orphans + return "$st" +} + # 冷检出刷导入(.godot 不入 git,fresh worktree 首跑一次性付) if [ ! -f "$GAME/.godot/uid_cache.bin" ]; then echo "[probe_gate] 冷检出:editor --quit 刷导入(一次性,可到 1-2 分钟)" @@ -86,39 +116,39 @@ pre="$(orphans | wc -l | tr -d ' ')" if [ "$pre" -gt 0 ]; then echo "[probe_gate] 预清本闸残留 Godot×$pre"; kill_orphans; fi echo "======== ui_input_probe ========" -timeout 120 "$G" --headless --path "$GAME_WIN" res://probe/ui_input_probe.tscn >"$TMP/uip.log" 2>&1 -st=$?; kill_orphans +run_suite 120 "$TMP/uip.log" res://probe/ui_input_probe.tscn +st=$? grep -E "PASS|FAIL|问题" "$TMP/uip.log" | head -12 verdict "ui_input_probe" "$TMP/uip.log" "$st" || _rc=1 echo "======== ui_struct_probe ========" -timeout 120 "$G" --headless --path "$GAME_WIN" res://probe/ui_struct_probe.tscn >"$TMP/stp.log" 2>&1 -st=$?; kill_orphans +run_suite 120 "$TMP/stp.log" res://probe/ui_struct_probe.tscn +st=$? grep -E "PASS|FAIL" "$TMP/stp.log" | head -12 verdict "ui_struct_probe" "$TMP/stp.log" "$st" || _rc=1 echo "======== layout_probe(分辨率几何·两档) ========" -timeout 120 "$G" --headless --path "$GAME_WIN" res://probe/layout_probe.tscn >"$TMP/lay.log" 2>&1 -st=$?; kill_orphans +run_suite 120 "$TMP/lay.log" res://probe/layout_probe.tscn +st=$? grep -E "PASS|FAIL" "$TMP/lay.log" | head -12 verdict "layout_probe" "$TMP/lay.log" "$st" || _rc=1 echo "======== ui_contrast_probe ========" -timeout 120 "$G" --headless --path "$GAME_WIN" res://probe/ui_contrast_probe.tscn >"$TMP/ctp.log" 2>&1 -st=$?; kill_orphans +run_suite 120 "$TMP/ctp.log" res://probe/ui_contrast_probe.tscn +st=$? grep -E "PASS|FAIL" "$TMP/ctp.log" | head -12 verdict "ui_contrast_probe" "$TMP/ctp.log" "$st" || _rc=1 echo "======== screen_flow_probe(L3 屏壳切换·迭代 29 T4) ========" -timeout 120 "$G" --headless --path "$GAME_WIN" res://probe/screen_flow_probe.tscn >"$TMP/sfp.log" 2>&1 -st=$?; kill_orphans +run_suite 120 "$TMP/sfp.log" res://probe/screen_flow_probe.tscn +st=$? grep -E "screen_flow_probe" "$TMP/sfp.log" | tail -3 verdict "screen_flow_probe" "$TMP/sfp.log" "$st" "screen_flow_probe" || _rc=1 if [ "$WITH_CLICK" -eq 1 ]; then echo "======== ui_click_probe 行为走查 ========" - timeout 240 "$G" --headless --path "$GAME_WIN" res://probe/ui_click_probe.tscn >"$TMP/clk.log" 2>&1 - st=$?; kill_orphans + run_suite 240 "$TMP/clk.log" res://probe/ui_click_probe.tscn + st=$? grep -E "click_probe" "$TMP/clk.log" | tail -4 verdict "ui_click_probe" "$TMP/clk.log" "$st" "click_probe" || _rc=1 fi @@ -127,26 +157,26 @@ fi # ui_tune_probe 49.6s/t17_defer_probe 63.0s/fx_probe 21.9s,timeout 120=2-7×余量) ── if [ "$WITH_FULL" -eq 1 ]; then echo "======== ui_probe(--full 收编) ========" - timeout 120 "$G" --headless --path "$GAME_WIN" res://probe/ui_probe.tscn >"$TMP/flp.log" 2>&1 - st=$?; kill_orphans + run_suite 120 "$TMP/flp.log" res://probe/ui_probe.tscn + st=$? grep -E "\[ui_probe\]" "$TMP/flp.log" | tail -2 verdict "ui_probe" "$TMP/flp.log" "$st" || _rc=1 echo "======== ui_tune_probe(--full 收编) ========" - timeout 120 "$G" --headless --path "$GAME_WIN" res://probe/ui_tune_probe.tscn >"$TMP/flt.log" 2>&1 - st=$?; kill_orphans + run_suite 120 "$TMP/flt.log" res://probe/ui_tune_probe.tscn + st=$? grep -E "\[tune_probe\]" "$TMP/flt.log" | tail -2 verdict "ui_tune_probe" "$TMP/flt.log" "$st" || _rc=1 echo "======== t17_defer_probe(--full 收编) ========" - timeout 120 "$G" --headless --path "$GAME_WIN" res://probe/t17_defer_probe.tscn >"$TMP/fld.log" 2>&1 - st=$?; kill_orphans + run_suite 120 "$TMP/fld.log" res://probe/t17_defer_probe.tscn + st=$? grep -E "\[t17_defer\]" "$TMP/fld.log" | tail -2 verdict "t17_defer_probe" "$TMP/fld.log" "$st" || _rc=1 echo "======== fx_probe(--full 收编) ========" - timeout 120 "$G" --headless --path "$GAME_WIN" res://probe/fx_probe.tscn >"$TMP/flx.log" 2>&1 - st=$?; kill_orphans + run_suite 120 "$TMP/flx.log" res://probe/fx_probe.tscn + st=$? grep -E "\[fx_probe\]" "$TMP/flx.log" | tail -2 verdict "fx_probe" "$TMP/flx.log" "$st" || _rc=1 fi diff --git a/待办清单.md b/待办清单.md index fc8c0dd8..3b212f24 100644 --- a/待办清单.md +++ b/待办清单.md @@ -538,7 +538,7 @@ #### 闸与探针批上下文挂死防护——probe_gate 进程树回收与 run_all 阻塞面超时(2026-10-25 登记,来源=批次移交) **实况**:证据=迭代 36 批末闸机观测(非本批引入)。原登记两件存量噪声,件①(battle_log_view.gd:17 类型声明错致运行期 TypedArray 错误、日志失真)已随迭代 38 卡⑥ 收口出账(git 提交 ce49a8ee),本条只剩件②:`level_shop_economy_test` 在闸批量上下文存在挂死形态(直跑恒绿),`tools/probe_gate.sh` 的 per-suite timeout 只杀 wrapper 不杀引擎子进程,孤儿泄漏会殃及后续全部 Godot 启动(批末四次闸跑批中断的根源,处置=清杀泄漏进程+删重建 `game/.godot`)。挂死防护=闸壳/测试基建,按改动四类属**工程改动走直改通道**。 -- 直改落点:tools/probe_gate.sh(per-suite 进程树回收)+game/tests/run_all.gd(OS.execute 阻塞面超时守护)——测试基建直改 +- 直改落点:tools/probe_gate.sh(per-suite 进程树回收)+game/tests/run_all.gd(OS.execute 阻塞面超时守护)——测试基建直改。**2026-10-28 用户裁四批全做,probe_gate 件已落**(run_suite 整树回收:启动记 WINPID → 超时先 `taskkill /T` 杀全树再 `kill -9` 收 MSYS 侧〔顺序颠倒会让 /T 走不到已死父进程〕→ 标志文件判超时〔taskkill 强杀后 MSYS wait 可能报 0〕;默认五段实跑全绿+超时/正常双路径机制验证通过)。run_all.gd 件属代码面(game/tests),按车道铁律留待触及测试基建的 worktree 批顺带,本条目保持待执行。 **改动面**:工具代码