diff --git a/AGENTS.md b/AGENTS.md index 0a7fb8b8..85feb897 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,7 +16,7 @@ ## 运行与验证(工作目录 = 含 project.godot 的切片目录) -- **交付前验收 = 跑 `tools/run_all_gates.sh`,按本批改动域选档(`--code` 只跑代码域/`--docs` 只跑文档域/不带参数=两域全量),所选域全绿才合回 master**——它是全部验证闸的唯一入口与唯一清单,**域划分也只写在它这一个地方**,闸件增减与分域调整都只改它,文档禁另列清单。域按**文件性质**判:代码域=`game/` 下非 `.md` 的行为载体(代码/测试脚本/探针/场景/数据表/素材),`game/` 内 `.md` 与 `game/` 外一切路径=文档域;同批两类都有时跑全量档。跑完写回执 `game/tests/.gates/receipt.json`(含 `domain` 字段),合回时由 worktree-guard 按 diff 文件性质推出该验哪个域再校验回执(六况拦截细则见 process-conventions §3.9);非代码面的直改批验收=commit 点 git 钩子自动跑 `--docs`,无 worktree 与回执流程 +- **交付前验收 = 跑 `tools/run_all_gates.sh`,按本批改动域选档(`--code` 只跑代码域/`--docs` 只跑文档域/不带参数=两域全量),所选域全绿才合回 master**——它是全部验证闸的唯一入口与唯一清单,**域划分与选闸算法也只写在它这一个地方**,闸件增减与分域调整都只改它,文档禁另列清单。域按**文件性质**判:代码域=`game/` 下非 `.md` 的行为载体(代码/测试脚本/探针/场景/数据表/素材),`game/` 内 `.md` 与 `game/` 外一切路径=文档域;同批两类都有时跑全量档。代码域按**关联档选测**(2026-10-29 用户裁):02 单元与集成只跑「需求卡片申报 ∪ diff 推导」并集的套件(关联档=只跑与本批改动关联的那部分测试),00 冒烟/01 系统测试/09 静态扫描三件保险恒跑不降,横切面改动(公共件/数据表/autoload/数值·存档·RNG·结算触面)回落全部套件;算法与套件族映射表只在闸脚本内定义。所选域全绿即可合回;全量回归由用户自发发起,合入后无自动兜底。跑完写回执 `game/tests/.gates/receipt.json`(含 `domain` 与 `scope` 字段),合回时由 worktree-guard 按 diff 文件性质推出该验哪个域再校验回执(六况拦截细则见 process-conventions §3.9);非代码面的直改批验收=commit 点 git 钩子自动跑 `--docs`,无 worktree 与回执流程 - 单件直跑(调试期最窄确认用,**不构成验收**):冒烟测试 `python tools/run_smoke.py`(`game/tests/smoke_boot.tscn` 极小启动场景)· 系统测试 `python tools/run_system_test.py`(宿主 `game/tests/system_test.gd`)· 场景探针 `…console.exe --headless --path . res://probe/ui_probe.tscn` · 单元与集成 `… --script res://tests/run_all.gd` - 调参批量:`--script res://sim/adventure_monte_carlo.gd`(全冒险期望)· `res://sim/reward_expectation.gd`(奖励期望);关卡完成率/难度类测量一律走全冒险 - 数值验证:`python tools/balance_verifier.py`(采集端 `res://sim/verifier_batch.gd`,报告与原始数据落 `sim-data/`) @@ -43,7 +43,7 @@ | 批次开工 / 流程纪律 | `design/engineering/process-conventions.md`——车道铁律(代码面 worktree 批/其余主区直改)、五段流水线、批次清单、测试两入口与 MC/闸机细则、像素纪律 | | 流水线段入口 | 技能对照——doc-design=文档设计(兼任反哺点落文)/asset-ingest=美术录入/req-card=写卡/iter-plan=迭代编排/iter=实现(五者均装于项目 `.zcode/skills/`);登记不立技能;批后收尾与中间文档退役=batch-closure(同一目录);格式与状态机权威=`design/engineering/doc-conventions.md` §12 | | 定代码方案 / 写代码 / 改代码前 | `design/engineering/code-conventions.md`——结构/契约/语言/数值/流程五类硬纪律(反例库 = 同目录 code-conventions-cases.md) | -| 定测试方案 / 写测试 / 改测试前 | `design/engineering/test-conventions.md`——探针标配 / 回归断言 / 全量验证 / 验收清单落码 | +| 定测试方案 / 写测试 / 改测试前 | `design/engineering/test-conventions.md`——探针标配 / 回归断言 / 提交前验证 / 验收清单落码 | | 定文档方案 / 写文档 / 改文档前 | `design/engineering/doc-conventions.md`——白话写作 / 载体分工 / 用语 / 结构骨架 / 会话记录与生命周期 | | 概念切分与互链 | `design/engineering/concept-conventions.md`——概念封装 / 跨文档引用 / 归档与事实源 | | 系统设计 / 增删文档 | `design/gdd/02-系统清单.md`——根索引 + 中文规范名↔英文代号对照 | @@ -84,7 +84,7 @@ - 车道铁律(判据与改动域划分同一,按文件性质切)——代码面(`game/` 下非 `.md` 的行为载体)唯一强制独立 worktree(迭代批全套仪式不变);其余一切面(含 `game/` 内 `.md` 与 `.zcode/`、`.qoder/` 等不入 git 面)主工作区直改+直提,commit 点由 git pre-commit 钩子(`tools/hooks/pre-commit`,`core.hooksPath` 挂载)现场跑文档域闸,绿落地红拦;主区直提代码面被该钩子拦;`--no-verify` 仅属用户,agent 禁用(细则见 process-conventions §1) - **仓库根禁止设会话状态文件**(`active.md` 已废除)——现状看 roadmap「当前位」,待办看待办清单,过程看 git 提交信息,产出物看 `reports/`;任何文档禁止另设「当前状态快照」节,两处记同一件事必然漂移(细则见 doc-conventions 第 10 节) - 测试执行两入口铁规——只有迭代 loop 开发与用户点名两个触发入口;其余会话零测试,需测试支撑先请示并注明未跑测 -- 交付前验收 = 跑 `tools/run_all_gates.sh` **按改动域**全绿(唯一入口、唯一闸清单、唯一域划分,禁在文档另列);合回 master 由 worktree-guard 按 diff 文件性质推域校验回执强制(process-conventions §3);非代码面直改批的验收=commit 点 git 钩子自动跑 `--docs`(process-conventions §1) +- 交付前验收 = 跑 `tools/run_all_gates.sh` **按改动域**全绿(唯一入口、唯一闸清单、唯一域划分与选闸算法,禁在文档另列);代码域按**关联档选测**——02 单元与集成跑「卡片申报 ∪ diff 推导」并集套件、三件保险(冒烟/系统测试/静态扫描)恒跑、横切面回落全量,所选域绿即可合回,全量回归由用户自发(2026-10-29 用户裁,合入后无自动兜底)(算法见闸脚本);合回 master 由 worktree-guard 按 diff 文件性质推域校验回执强制(process-conventions §3);非代码面直改批的验收=commit 点 git 钩子自动跑 `--docs`(process-conventions §1) - **改动四类**——**需求改动**(游戏代码,含随逻辑写/改的测试代码):唯一走「写需求卡片+编入迭代批次」通道;迭代实现批禁写 `design/` 整个目录、两份根流程清单(`待办清单.md`/`体验验证清单.md`)与 `game/tests/README.md`(能力清单,归写卡段维护)(含新增/修改/删除),设计文档对它只读、是写测试与实现代码的输入依据;两份流程清单的条目状态只由流水线各段推进,实现批禁止顺手改;产出物只准写 `reports/`;批末只跑代码域闸。**工程改动**(tools/、闸件、模拟测量工具、纯测试维护——不改变游戏行为的配套代码)、**文档设计改动**、**美术素材改动**:不写需求卡片、不编迭代批次,登记在案后由用户发令在主工作区直改+直提(commit 点 git 钩子自动跑文档域闸),完成即删条目;触及代码面的工程改动(如纯测试维护)仍走 worktree 批;文档设计类写入走 doc-design 技能(登记条目格式由流水线闸强制) - 五段流水线铁律——**需求改动**(游戏代码)按「登记 → 文档设计/美术设计 → 写需求卡片 → 迭代编排 → 实现」五段推进,段间不自动流转、每段转进由用户发令;编入迭代的卡片必须设计闭合+零待裁点——需求卡片任何工作量皆可写、皆可编入迭代,工作量限制只属迭代层(迭代估时总量 <5 个工作日=「冻结待凑批」、不可开工);流水线闸 `tools/pipeline_gate.py` 强制(已注册进 run_all_gates.sh 文档域);实现段零裁断——卡片没写清=上游缺陷,停该卡退回上游,禁内联求裁(process-conventions §2,格式与状态机=doc-conventions §12)。工程/文档设计/美术素材三类改动不走写卡与编排段:登记 →(如需设计讨论)→ 用户发令直改 → 主区直改+commit 点闸 → 完成删条目(车道细则=process-conventions §1) - MC/模拟:无数值改动零跑;档位硬闸见 `sim-data/README.md`「MC 采集档位表」,一档只能由用户发起 diff --git a/design/engineering/code-conventions.md b/design/engineering/code-conventions.md index 26b24b42..7f7f07a9 100644 --- a/design/engineering/code-conventions.md +++ b/design/engineering/code-conventions.md @@ -54,7 +54,7 @@ ## 3. 测试纪律 -已独立成文:`design/engineering/test-conventions.md`(探针标配 / 回归断言 / 提交前全量验证 / 验收清单落码规范)。 +已独立成文:`design/engineering/test-conventions.md`(探针标配 / 回归断言 / 提交前验证 / 验收清单落码规范)。 --- diff --git a/design/engineering/process-conventions.md b/design/engineering/process-conventions.md index 740a2dbd..727fc342 100644 --- a/design/engineering/process-conventions.md +++ b/design/engineering/process-conventions.md @@ -48,7 +48,7 @@ 7. **验收改动先行声明**——验收变化(基线换代/探针增改/旧断言改判)预先写进需求卡片,禁事后补 **完成** -8. **批末按域验收才合并(worktree 批;直改车道无批末动作,验收在 commit 点由 git 钩子执行,见 §1 第 3 条)**——在任务 worktree 内跑 `tools/run_all_gates.sh`,**按本批改动域选档**:按文件性质判——代码面=`game/` 下非 `.md` 的行为载体(代码、测试脚本、探针、场景、数据表、素材)→ `--code`;文档件=`game/` 内 `.md`(README、生成清单)与 `game/` 外一切路径(`design/`、根规则文件、`tools/` 工具脚本、`.zcode/` 配置等)→ `--docs`;两类同批都有 → 全量档不带参数。所选域全绿才合回 master。合回前独立验收审计通过(iter 技能第 5 步),未过不合回。该脚本是全部验证闸的**唯一入口、唯一清单与唯一域划分**,闸件增减、分域调整都只改它;**本文与其他文档禁另列闸件清单**(清单散落多处正是 iter27 漏跑 `probe_gate --full` 的成因)。跑完写回执 `game/tests/.gates/receipt.json`(含 `domain` 与 `dirty_game` 字段),合回时由 worktree-guard 按 diff 文件性质推域强制校验(六况拦截:缺回执/quick 模式/回执未覆盖分支尖=回执 commit 之后代码面又有改动、或跑闸时代码面带着未提交改动(`dirty_game`)/域不覆盖本次改动/含新增红/master HEAD 非回执 commit 祖先=合并将产出未测内容)。回执与分支尖的对齐判据是**代码面**不是 commit 号:回执之后只追加文档件(`game/` 内 `.md`、`reports/`、`design/` 等)提交,回执照样有效,不构成复跑理由。闸件(`tools/` 下机检脚本与闸壳)自身的改动不占合回全量——其质量义务=落批时人工确认闸可跑通且判据口径未变,闸真崩溃时合回闸 fail-closed(跑不出回执即拦),不以合回全量为兜底 +8. **批末按域验收才合并(worktree 批;直改车道无批末动作,验收在 commit 点由 git 钩子执行,见 §1 第 3 条)**——在任务 worktree 内跑 `tools/run_all_gates.sh`,**按本批改动域选档**:按文件性质判——代码面=`game/` 下非 `.md` 的行为载体(代码、测试脚本、探针、场景、数据表、素材)→ `--code`;文档件=`game/` 内 `.md`(README、生成清单)与 `game/` 外一切路径(`design/`、根规则文件、`tools/` 工具脚本、`.zcode/` 配置等)→ `--docs`;两类同批都有 → 全量档不带参数。代码域按**关联档选测**(2026-10-29 用户裁):02 单元与集成只跑「需求卡片申报 ∪ diff 推导」并集的套件——申报=在册需求卡片对照表「验收测试」列提到的测试文件名,推导=闸脚本内套件族映射表按改动文件归族;改动命中公共件/数据表/autoload/数值·存档·RNG·结算触面或映射不到 → 横切回落全部套件;00 冒烟/01 系统测试/09 静态扫描三件保险恒跑不降;03/04 探针与 05 渲染闸维持影响面口径(本批 game/ 改动全在测量面 game/tests、game/sim 时记 SKIP)。所选域全绿即可合回 master,全量回归由用户自发发起(合入后无自动兜底)。合回前独立验收审计通过(iter 技能第 5 步),未过不合回。该脚本是全部验证闸的**唯一入口、唯一清单与唯一域划分(含选闸算法与套件族映射表)**,闸件增减、分域调整都只改它;**本文与其他文档禁另列闸件清单**(清单散落多处正是 iter27 漏跑 `probe_gate --full` 的成因)。跑完写回执 `game/tests/.gates/receipt.json`(含 `domain`、`scope`(full|related,02 跑子集=related)与 `dirty_game` 字段),合回时由 worktree-guard 按 diff 文件性质推域强制校验(六况拦截:缺回执/quick 模式/回执未覆盖分支尖=回执 commit 之后代码面又有改动、或跑闸时代码面带着未提交改动(`dirty_game`)/域不覆盖本次改动/含新增红/master HEAD 非回执 commit 祖先=合并将产出未测内容;guard 只验域覆盖与全绿,不复核选套件清单)。回执与分支尖的对齐判据是**代码面**不是 commit 号:回执之后只追加文档件(`game/` 内 `.md`、`reports/`、`design/` 等)提交,回执照样有效,不构成复跑理由。闸件(`tools/` 下机检脚本与闸壳)自身的改动不占合回全量——其质量义务=落批时人工确认闸可跑通且判据口径未变,闸真崩溃时合回闸 fail-closed(跑不出回执即拦),不以合回全量为兜底 9. **合并后主区零复跑**(2026-10-24 用户裁:合回重跑验的是与回执同一份字节,零信息)——合回落地后 code 批只做两件秒级事:`--editor --quit` 重刷类名缓存(在册坑:.godot 缓存不入 git,与合并后代码失配会假红假绿)+冒烟启动确认可开机(合计约 1 分钟);docs 批零动作。全量验证**每批跑一次**=worktree 批末按域闸,顺序是「先并入 master、再跑闸」(§1 第 2 条);只有两种情形允许把这次跑作废重来——① 闸红且修法动了 `game/`,② 闸已开跑后 master 又被推进(合并将产出未测内容,由合回闸祖先判定拦下,§3.9 第六况)。「跑都跑了再跑一遍保险」「闸后顺手改点 `game/` 再补一跑」都不构成复跑理由;master 上永不跑全量 10. **批次完成后的登记义务**——合回 master 后按 doc-conventions §12.1 推进待办清单条目状态(做完整条删除),涉及迭代编排的在 roadmap 迭代总表与顶部「当前位」节换代——执行入口=项目技能 batch-closure(批后收尾与中间文档退役),跨会话教训写记忆库。批次过程与结论由 git 提交信息承载,禁止再写会话状态文件(见 doc-conventions 第 10 节)。`design/` 与根规则文件(`AGENTS.md`/`roadmap.md`/`待办清单.md`/`体验验证清单.md`)实现批禁写,见 §12 11. **文档同步闸(实现批禁写文档)**——实现批**禁写 `design/` 整个目录与根规则文件**,**另禁写 `game/tests/README.md`(能力清单,归写卡段维护,test-conventions §8.9)**,要往外交代的东西一律落 `reports/`:实现报告 `reports/iter-实现报告-<日期>.md`(各卡状态+实机目检总清单+遗留读数义务+实现决定记录——超白名单/超卡片明列范围事项逐条记「决了什么/为什么/代价/撤销方式」,全部按卡执行时只写一行「零拍板」)、GDD 反哺点清单 `reports/iter-反哺点-<日期>.md`(每条写清哪个文件哪一节、改成什么、依据是什么)。设计权威文档(GDD/art-bible)**任何批都不直改**:用户在会话中逐点裁决后,由工程/文档设计改动批(直改类)写入 `design/`(执行入口=doc-design 技能),并把反哺条目从 `reports/` 清掉(吸收即死;退役执行=batch-closure 技能)。机械面文档(roadmap 行/术语表/README 工具注)同样归工程/文档设计改动批在合并前更新。反哺点未落地期间相关 GDD 节视为待反哺,读前先查 `reports/` 与待办清单;需求卡片必列「关联文档同步面」按两层标注 diff --git a/design/engineering/test-conventions.md b/design/engineering/test-conventions.md index 83c08e47..b30c0c47 100644 --- a/design/engineering/test-conventions.md +++ b/design/engineering/test-conventions.md @@ -25,9 +25,9 @@ 每个缺陷修复必须在探针或冒烟中留下能抓住该缺陷的断言(例:封印动作耗尽 AP → 必须进入第 2 轮; 选用道具 → 库存态必须变 Used)。没有回归断言的修复视为未完成。 -## 4. 提交前全量验证 +## 4. 提交前验证 -交付前跑 `tools/run_all_gates.sh`(不带 `--quick`)全绿——该脚本是全部验证闸的唯一入口与唯一清单,本文禁另列闸件清单。闸件增减只改脚本。 +交付前跑 `tools/run_all_gates.sh`(不带 `--quick`),按改动域选档、所选域全绿——该脚本是全部验证闸的唯一入口与唯一清单,域划分与选闸算法只写在脚本里,本文禁另列闸件清单。闸件增减只改脚本。代码域按**关联档选测**(2026-10-29 用户裁):02 单元与集成只跑「需求卡片申报 ∪ diff 推导」并集的套件,00 冒烟/01 系统测试/09 静态扫描三件保险恒跑不降,横切面改动(公共件/数据表/autoload/数值·存档·RNG·结算触面)回落全部套件;所选域全绿即可合回,全量回归由用户自发发起(合入后无自动兜底)。 行为探针使用随机化玩家行为(封印/停手/刷新/无效点击)——自动玩家不做的动作就是测试盲区。 ## 5. 测试函数怎么写——验收清单条目的落码规范 diff --git a/game/tests/README.md b/game/tests/README.md index 8517ec9f..9163d0b1 100644 --- a/game/tests/README.md +++ b/game/tests/README.md @@ -2,7 +2,7 @@ **Engine**: Godot 4.7.2-stable(GDScript 单语言) **Test Framework**: 标准四级测试层(冒烟/单元/集成/系统)+ headless 场景探针与渲染 golden;gdUnit4 **deferred**(D2 OQ1 关闭形态——本仓库不引第三方测试框架,需求见下「门禁」节) -**门禁**: 本地闸——`tools/run_all_gates.sh`(收口唯一验收入口,按改动域选档 `--code`/`--docs`,跨两域或改闸件本身不带参数=全量)。本仓库**无远端、无 CI**(`.github/workflows/` 已删除,不建不留不参考);闸清单与域划分只在本脚本定义,文档不另列 +**门禁**: 本地闸——`tools/run_all_gates.sh`(收口唯一验收入口,按改动域选档 `--code`/`--docs`,跨两域或改闸件本身不带参数=全量;代码域 02 按关联档选套件,算法与套件族映射表只在脚本内定义)。本仓库**无远端、无 CI**(`.github/workflows/` 已删除,不建不留不参考);闸清单与域划分只在本脚本定义,文档不另列 **Setup date**: 2026-09-12(2026-09-27 `7f24b640` 三层收敛:根 tests/ 并入 `game/tests/`、docs 并入 `design/engineering/`、production 解散) **参考实现**: `game/probe/`(场景探针族)· `game/sim/`(测量/诊断/模拟驱动件)(场景探针族——正式工程的活样板;系统测试断言面过渡宿主 `smoke_all.gd` 亦在此,迁移归断言迁移批) @@ -197,7 +197,7 @@ tests/ |---|---|---|---| | `tools/run_smoke.py`(冒烟测试) | 这版能否启动、能否加载表、能否进主屏——零逻辑判定 | 退出码+脚本错误扫描 | 开机路径/表加载改动后的快速自检;收口由闸编排 | | `tools/run_system_test.py`(系统测试) | 真实玩法路径的行为断言面(推理→战斗→结算→进度推进) | 退出码+脚本错误扫描 | 玩法链路改动的调试期确认 | -| `tests/run_all.gd`(单元与集成) | 公式/状态机/契约/跨系统闭环(64 套件 manifest) | 退出码+三硬闸(见下);每套件子进程带截止时间守护——超时先 taskkill /T 整树回收再判该套件 FAIL、批继续(默认 120s,环境变量 `RUN_ALL_SUITE_TIMEOUT_SEC` 可覆盖) | 新套件单跑调试;逻辑改动的最小单元复跑;开发期选跑——追加 `-- --only=<词[,词…]>` 只跑路径命中词的套件(零命中=用法错 exit 2;选跑态仅供开发期最窄确认,不产闸回执、不作验收凭据) | +| `tests/run_all.gd`(单元与集成) | 公式/状态机/契约/跨系统闭环(65 套件 manifest) | 退出码+三硬闸(见下);每套件子进程带截止时间守护——超时先 taskkill /T 整树回收再判该套件 FAIL、批继续(默认 120s,环境变量 `RUN_ALL_SUITE_TIMEOUT_SEC` 可覆盖);连续 3 个套件超时=红树整批挂死,第 3 个即中止整轮、汇总已见失败与超时清单后非 0 退出(阈值=可调常量,不连续的超时照旧逐个回收) | 新套件单跑调试;逻辑改动的最小单元复跑;开发期选跑——追加 `-- --only=<词[,词…]>` 只跑路径命中词的套件(零命中=用法错 exit 2;此形态仅供开发期最窄确认,不产闸回执、不作验收凭据);闸机关联档选跑——同一过滤机制加 `-- --only-source=gates`,由 `tools/run_all_gates.sh` 在 02 闸传入(回执 scope=related,是验收跑批本体) | | `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 行) | 改产品码前后的快速自检;新增纪律先补扫描规则再修码 | @@ -225,7 +225,9 @@ godot --headless --path . res://probe/.tscn # 挂死防护内建于 run_all.gd(引擎无死锁探测,超时靠外部判):每套件子进程带截止时间—— # 默认 120s,环境变量 RUN_ALL_SUITE_TIMEOUT_SEC 可覆盖;超时先 taskkill /T 整树回收 -#(cmd wrapper 连引擎孙进程一起杀,不留孤儿),该套件判 FAIL、批继续——单套件挂死不拖死整闸 +#(cmd wrapper 连引擎孙进程一起杀,不留孤儿),该套件判 FAIL、批继续——单套件挂死不拖死整闸; +# 连续 3 个套件超时=红树整批挂死,第 3 个即中止整轮(汇总已见失败与超时清单、退出码非 0), +# 不连续的超时照旧逐个回收;阈值是 run_all.gd 内可调常量 ``` > 套件 manifest 现况:`_suites()` 登记 **65 条**(62 单元+3 集成,2026-10-29 实数;口径=manifest 条目数, @@ -297,9 +299,9 @@ S12 效果引擎无状态化旧件零残留:扫描 game/ 全域 `.gd` 与 `gam ## 本地闸(唯一验收门禁) -本仓库无远端、无 CI——**验收只在本地发生**,跑 `tools/run_all_gates.sh` 是收口唯一入口,按本批改动域选档(`--code`/`--docs`/跨两域或改闸件本身=不带参数全量), -跑完写回执 `game/tests/.gates/receipt.json`(含 `domain` 字段),合回 master 由 worktree-guard 按 diff 文件性质推域校验回执(`game/` 内 `.md` 归文档域)。 -闸清单与域划分的唯一定义处就是那个脚本,本文档不另列清单(清单散落多份文档正是历史漏跑闸的成因)。 +本仓库无远端、无 CI——**验收只在本地发生**,跑 `tools/run_all_gates.sh` 是收口唯一入口,按本批改动域选档(`--code`/`--docs`/跨两域或改闸件本身=不带参数全量;代码域 02 按关联档选套件,回执 `scope` 字段记 full|related), +跑完写回执 `game/tests/.gates/receipt.json`(含 `domain` 与 `scope` 字段),合回 master 由 worktree-guard 按 diff 文件性质推域校验回执(`game/` 内 `.md` 归文档域)。 +闸清单与域划分(含选闸算法与套件族映射表)的唯一定义处就是那个脚本,本文档不另列清单(清单散落多份文档正是历史漏跑闸的成因)。 **判定三硬闸**(各闸一律适用,缺一即判定有洞): ①**退出码为唯一权威判据**——日志里的 PASS 串只是日志,不作判据; diff --git a/game/tests/run_all.gd b/game/tests/run_all.gd index 52c01ca9..c922897f 100644 --- a/game/tests/run_all.gd +++ b/game/tests/run_all.gd @@ -1,8 +1,9 @@ # tests/run_all.gd — 单元测试统一入口(双载体的逻辑测试调用点,套件 manifest 协议 D2 AC-2b) # 用法:godot --headless --path game --script res://tests/run_all.gd -# 选跑态(开发期直跑专用):追加 `-- --only=<词[,词…]>` ——对套件 res:// 路径做子串匹配,只跑命中套件; -# 零命中/空值=用法错 quit(2)。选跑态仅供开发期最窄确认,不产闸回执、不作验收凭据; -# 批末全量闸不带参数不经选跑分支。 +# 选跑态:追加 `-- --only=<词[,词…]>` ——对套件 res:// 路径做子串匹配,只跑命中套件; +# 零命中/空值=用法错 quit(2)。两个来源分开定性——开发期直跑(默认)仅供最窄确认,不产闸回执、 +# 不作验收凭据;闸机关联档选跑(--only-source=gates,由 tools/run_all_gates.sh 在 02 闸传入) +# 是收口验收跑批本体,回执 scope=related。无任何选跑参数=全量态,行为与全量口径完全一致。 # 协议:套件清单逐条"套件 → 期望退出码";致命类独立进程(expect=1)。 # 执行形态:逐套件起独立 Godot 子进程实跑 #(OS.get_executable_path() 解析本引擎二进制——--script 模式无 autoload/全局标识符, @@ -11,6 +12,9 @@ # 套件挂死(_init 在 quit() 前中断,子进程永不退出)=超时先 taskkill /T 整树回收 #(连 cmd wrapper 带引擎孙进程一起杀,只杀 wrapper 会留孤儿殃及后续全部 Godot 启动) # 再判该套件 FAIL 继续下一套件——单套件挂死不拖死整批;正常退出路径判定形态不变。 +# 快速失败:连续 N 个套件超时=红树正在整批挂死(如公共件解析错让每个套件都起不来), +# 继续逐个回收只剩烧钟——第 N 个超时即中止整轮,汇总已见失败与超时清单后非 0 退出; +# 不连续的超时仍逐个回收照旧。 # 场景探针(.tscn 载体)由本地闸(probe_gate.sh)单独步骤调用,不经本入口。 extends SceneTree @@ -23,21 +27,27 @@ const ERROR_MARKERS: Array[String] = ["SCRIPT ERROR", "Parse Error"] const SUITE_TIMEOUT_ENV := "RUN_ALL_SUITE_TIMEOUT_SEC" const SUITE_TIMEOUT_DEFAULT_SEC := 120 +# 快速失败阈值(可调常量,初值 3):连续 N 个套件超时即中止整轮。 +const CONSECUTIVE_TIMEOUT_ABORT := 3 + func _init() -> void: - # 选跑态解析:--only=<词[,词…]>(经 -- 分隔的 user args)——对套件 res:// 路径做子串匹配。 - # 空值/零命中=用法错 quit(2)(fail-closed:禁把零套件当绿);选跑态仅供开发期最窄确认, - # 不产闸回执、不作验收凭据;批末全量闸不带参数,不走此分支。 + # 选跑态解析:--only=<词[,词…]>(经 -- 分隔的 user args)——对套件 res:// 路径做子串匹配; + # --only-source=gates=来源标记:由闸脚本传入的关联档选跑(验收跑批本体),缺省=开发期直跑。 + # 空值/零命中=用法错 quit(2)(fail-closed:禁把零套件当绿)。 var only_terms := PackedStringArray() var has_only := false + var only_from_gates := 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) + elif a == "--only-source=gates": + only_from_gates = true if has_only and only_terms.is_empty(): - push_error("[run_all] --only= 空值——用法错(选跑态禁把零套件当全量)") + push_error("[run_all] --only= 空值——用法错(选跑禁把零套件当全量)") quit(2) return var manifest := _suites() @@ -50,15 +60,37 @@ func _init() -> void: quit(2) return filtered = true - print("[run_all] FILTERED RUN — 选跑 %d/%d 套件(--only=%s)——仅供开发期最窄确认,不产闸回执、不作验收凭据" % [suites.size(), manifest.size(), ",".join(only_terms)]) + if only_from_gates: + print("[run_all] GATE FILTERED RUN — 闸机关联档选跑 %d/%d 套件(--only=%s)" % [suites.size(), manifest.size(), ",".join(only_terms)]) + else: + print("[run_all] FILTERED RUN — 选跑 %d/%d 套件(--only=%s)——仅供开发期最窄确认,不产闸回执、不作验收凭据" % [suites.size(), manifest.size(), ",".join(only_terms)]) 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 + var consecutive_timeouts := 0 + var failed_paths := PackedStringArray() + var timed_out_paths := PackedStringArray() for suite in suites: - failures += _run_suite(suite, ran, filtered) + var outcome := _run_suite(suite, ran, filtered) + var suite_fail: int = outcome["fail"] + var suite_timed_out: bool = outcome["timed_out"] + failures += suite_fail ran += 1 + var spath: String = suite.get("path", "") + if suite_timed_out: + consecutive_timeouts += 1 + timed_out_paths.append(spath) + if consecutive_timeouts >= CONSECUTIVE_TIMEOUT_ABORT: + push_error("[run_all] ABORT — 连续 %d 个套件超时(阈值 %d),中止整轮" % [consecutive_timeouts, CONSECUTIVE_TIMEOUT_ABORT]) + _report_abort(failed_paths, timed_out_paths, ran, suites.size()) + quit(1) + return + else: + consecutive_timeouts = 0 + if suite_fail > 0: + failed_paths.append(spath) # 实跑套件数校验:清单有条目却被静默跳过(早退/continue)必须判红,不能只凭 failures。 # 选跑态对过滤后清单校验;全量态与原 manifest 条目数同一。 if ran != suites.size(): @@ -151,17 +183,19 @@ func _suites() -> Array: ] -func _run_suite(suite: Dictionary, idx: int, filtered: bool = false) -> int: +# 返回 {"fail": int(该套件失败计 1/过 0), "timed_out": bool(是否超时挂死)}—— +# timed_out 供编排层做连续超时快速失败判定,fail 照旧累计。 +func _run_suite(suite: Dictionary, idx: int, filtered: bool = false) -> Dictionary: var path: String = suite.get("path", "") var expect: int = int(suite.get("expect", 0)) if path.is_empty(): push_error("[run_all] suite missing path") - return 1 + return {"fail": 1, "timed_out": false} var bin := _engine_bin() if bin == "": push_error("[run_all] 未找到 console 版引擎二进制(设 GODOT_BIN 指向 *_console.exe)——" + "非 console 版是 GUI 子系统,子进程 stdout/stderr 句柄无效,脚本错误扫描无从落地") - return 1 + return {"fail": 1, "timed_out": false} # 子进程输出只能落盘再读:OS.execute 的 output 管道在本机 Godot/Windows 组合下恒为空 #(实测 0 行),故整条命令交给 cmd 并重定向到日志。退出码同样走文件通道—— # 阻塞式 execute 无超时能力(套件挂死时整批陪葬),换 create_process 非阻塞起进程 @@ -180,7 +214,7 @@ func _run_suite(suite: Dictionary, idx: int, filtered: bool = false) -> int: var pid: int = OS.create_process("cmd.exe", PackedStringArray(["/V:ON", "/C", cmd])) if pid < 0: push_error("[run_all] FAIL %s — cmd 子进程启动失败(pid=%d)" % [path, pid]) - return 1 + return {"fail": 1, "timed_out": false} # 轮询先查存活再查截止:进程在轮询间隙自然退出时按正常路径判,截止临界不误杀。 var timed_out := false var deadline_ms := Time.get_ticks_msec() + _suite_timeout_sec() * 1000 @@ -213,7 +247,7 @@ func _run_suite(suite: Dictionary, idx: int, filtered: bool = false) -> int: push_error("[run_all] FAIL %s — 超时挂死(%ds),进程树已整树回收(pid=%d)" % [path, _suite_timeout_sec(), pid]) else: push_error("[run_all] FAIL %s — 超时挂死(%ds),进程树回收未确认(pid=%d,需手工清杀残留)" % [path, _suite_timeout_sec(), pid]) - return 1 + return {"fail": 1, "timed_out": true} # 负面套件(坏表/违规输入是它的测试目标)全文折叠,只回显结论行—— # 它输出里的 Parse Error 是预期产物,回显出去会让编排层的日志扫描误判成真实脚本错误。 if negative: @@ -223,17 +257,26 @@ func _run_suite(suite: Dictionary, idx: int, filtered: bool = false) -> int: var code_txt := FileAccess.get_file_as_string(codefile).strip_edges() if FileAccess.file_exists(codefile) else "" if code_txt == "" or not code_txt.is_valid_int(): push_error("[run_all] FAIL %s — 退出码文件缺失或非数字(%s)——子进程未走到退出码落盘即死,判红" % [path, codefile]) - return 1 + return {"fail": 1, "timed_out": false} var code: int = int(code_txt) DirAccess.remove_absolute(codefile) # 用毕即删:退出码经文件通道但不落 residuum(.gates 只留 *.log) if not negative and _has_script_error(text): push_error("[run_all] FAIL %s — 输出含脚本错误标记:%s" % [path, _script_error_line(text)]) - return 1 + return {"fail": 1, "timed_out": false} if code != expect: push_error("[run_all] FAIL %s — 退出码 %d ≠ 期望 %d" % [path, code, expect]) - return 1 + return {"fail": 1, "timed_out": false} print("[run_all] PASS %s (exit=%d)" % [path, code]) - return 0 + return {"fail": 0, "timed_out": false} + + +# 快速失败中止时的汇总:已见失败与超时清单+中止时进度——红树几分钟出结论,不磨完整轮。 +func _report_abort(failed: PackedStringArray, timed_out: PackedStringArray, ran: int, total: int) -> void: + var timed_out_txt := "、".join(timed_out) if timed_out.size() > 0 else "(无)" + var failed_txt := "、".join(failed) if failed.size() > 0 else "(无)" + push_error("[run_all] 已见超时 %d 件:%s" % [timed_out.size(), timed_out_txt]) + push_error("[run_all] 已见失败(非超时)%d 件:%s" % [failed.size(), failed_txt]) + push_error("[run_all] 中止时进度:%d/%d 套件(余下未跑)" % [ran, total]) # 选跑过滤:对套件 res:// 路径做子串匹配,任一词命中即选入,词序无关。 diff --git a/tools/run_all_gates.sh b/tools/run_all_gates.sh index a2b5558b..97b625e0 100644 --- a/tools/run_all_gates.sh +++ b/tools/run_all_gates.sh @@ -14,8 +14,11 @@ # --code 只跑代码域闸(00 冒烟测试/01 系统测试/02 单元与集成测试/03 探针 --click/ # 04 探针 --full/05 渲染闸/09 静态扫描/11 回放确定性对拍+10 熵增闸) # ——本批改动含 game/ 下非 .md 的行为载体(代码/测试脚本/探针/场景/数据表/素材)时用。 -# 影响面选闸:本批 game/ 改动全在测量面(game/tests、game/sim)时,00/03/04/05 记 -# SKIP(测量面不触开机路径/场景树/渲染输出;映射定义在脚本中部,判定保守)。 +# 关联档选闸(2026-10-29 用户裁,推广自 2026-10-28 影响面选闸):00 冒烟/01 系统测试/ +# 09 静态扫描三件保险恒跑不降;03/04 探针与 05 渲染闸维持影响面口径(本批 game/ 改动 +# 全在测量面 game/tests、game/sim 时记 SKIP);02 单元与集成改「选套件跑」——套件集= +# 需求卡片申报 ∪ diff 推导并集(算法与套件族映射表见脚本中部),横切面(公共件/数据表/ +# autoload/数值·存档·RNG·结算触面)回落全套件;回执 scope 字段记 full|related。 # --docs 只跑文档域闸(06 gdd_lint/07 doc_link/08 doc_term/08c 流水线闸/08d 引用可核/ # 08e 能力审计+10 熵增闸) # ——本批改动只有文档件时用:game/ 内的 .md(README、生成清单)、design/、根规则文件、 @@ -154,12 +157,83 @@ 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,不作闸况变化。 +# PARITY 触面表——gate 11 数值·存档·RNG·结算触面判定与关联档横切判定共用同一清单(单点定义,勿在别处复制)。 +PARITY_TOUCH_FACES=" +game/data/tables/ +game/core/rng_service.gd +game/core/save_manager.gd +game/core/meta_progression.gd +game/core/settlement/ +game/core/damage_math.gd +game/core/damage_controller.gd +game/core/battle_orchestrator.gd +game/sim/adventure_monte_carlo.gd +" + +# ── 套件族映射表(关联档·推导半边的唯一事实源)── +# game/ 代码面文件 → tests/ 套件族词(--only 子串词,词=套件目录名)。机械近似:同族内 +# 套件覆盖面仍可能宽于被测文件,保守方向=宁多勿漏。横切判定(公共件前缀/project.godot/ +# PARITY 触面表)先于本表执行,故表中不收 damage_math/damage_controller/battle_orchestrator/ +# settlement/** 等数值·存档·RNG·结算触面文件——它们恒走横切回落全量。映射不到的文件返回空=横切处理。 +suite_terms_for() { # suite_terms_for <仓库相对路径> → stdout: 套件族词(空格分隔,可空) + case "$1" in + # 战斗结算域 + game/core/action_economy.gd|game/core/battle_fx_host.gd|game/core/effect_context.gd|\ + game/core/effect_engine.gd|game/core/gain_journal.gd|game/core/skill_lottery.gd) + echo "battle_settlement" ;; + game/core/reward_economy.gd|game/core/reward_grant.gd) + echo "battle_settlement core" ;; + game/core/status_container.gd) + echo "battle_settlement contracts" ;; + game/core/unit_container.gd) + echo "battle_settlement data_config" ;; + # 排雷/棋盘域(封印与极性行为跨战斗结算族) + game/core/board_data.gd|game/core/minesweeper_rules.gd) + echo "core battle_settlement" ;; + game/core/board_generator.gd|game/core/node_map_generator.gd|game/core/hero_stats.gd|\ + game/core/curio_deck.gd|game/core/curio_stop.gd|game/core/refresh_bank.gd|\ + game/core/treasure_items.gd|game/core/attune_loadout.gd|game/core/attunement.gd|\ + game/core/meta_state_store.gd|game/core/slot_store.gd|game/core/gp_wallet.gd|\ + game/core/shop_session.gd) + echo "core" ;; + # 冒险结构×存档断点×关卡推进(core 族+跨系统集成族) + game/core/adventure_runner.gd|game/core/level_runner.gd) + echo "core integration" ;; + # 配置域表类(autoload 本体与解析器/跨表校验器走横切,不入本表) + game/core/config_domain_*.gd) + echo "data_config core" ;; + # 数据 schema(data_config 族守卫+core 族消费侧校验与跨表校验) + game/data/schemas/*) + echo "data_config core" ;; + # 呈现域 + game/presentation/*) + echo "presentation" ;; + # 素材(字体/地形/主题类断言在 presentation 族;探针与渲染闸另有行为面闸管) + game/assets/*) + echo "presentation" ;; + *) + echo "" ;; + esac +} + +# ── 关联档选闸(2026-10-29 用户裁,推广自 2026-10-28 影响面选闸)── +# 三件保险恒跑不降:00 冒烟/01 系统测试/09 静态扫描(10 熵增闸照旧两域都跑,11 数值·存档·RNG·结算触面判定照旧)。 +# 03/04 探针与 05 渲染闸维持影响面口径:本批 diff 相对 master 分叉点的 game/ 改动若全部落在 +# 测量面(game/tests/**、game/sim/**——测试与模拟驱动件,不触场景树与渲染输出)则记 SKIP。 +# 02 单元与集成改「选套件跑」:套件集=申报 ∪ 推导 取并集—— +# 申报:在册需求卡片(本批 diff 触及的 reports/需求卡片*.md ∪ roadmap「进行中」迭代行登记的 +# 卡片)对照表「验收测试」列提到的测试文件名,按文件名作为 --only 子串词归套件; +# 找不到在册卡片则申报集为空并输出注明。 +# 推导:按下方套件族映射表把 game/ 代码面文件(非 .md、非测量面)归到套件族; +# 映射不到的文件一律按横切处理。 +# 横切回落全量:diff 命中 game/data/tables/**、公共件前缀(config_db*/rng_service*/ +# save_manager*/meta_progression*/main*——main* 覆盖组合根拆件与主场景)、 +# autoload 注册面(game/project.godot)、或下方 PARITY 触面表任一 → 02 跑全部套件。 +# 测量面批(game/ 改动全在 tests/sim):02 照旧全套件跑(不加过滤)。 +# 回执 scope 字段:full|related(02 跑子集=related);映射表只在本脚本一处定义。 NONBEHAVIOR=0 +SCOPE="full" +ONLY_TERMS="" 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)" @@ -167,7 +241,62 @@ if in_domain code; then _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" + echo "[gates] 影响面选闸:game/ 改动全在测量面(tests/sim)——03/04 探针/05 渲染闸 → SKIP(00/01/09 三件保险恒跑)" + fi + # ── 02 选套件推导(只对行为面代码文件做——测量面文件不进推导,其验证就是 02/09 自身)── + _codefiles="$(echo "$_gfiles" | grep -viE '\.md$' | grep -vE '^(game/tests/|game/sim/)' || true)" + if [ -n "$_codefiles" ]; then + CROSSCUT=0 + _family_terms="" + # 逐文件判定:横切命中即整批回落全量;否则按映射表归族。 + while IFS= read -r _f; do + [ -z "$_f" ] && continue + _b="$(basename "$_f")" + case "$_b" in + config_db*|rng_service*|save_manager*|meta_progression*|main*) + CROSSCUT=1; break ;; + esac + [ "$_f" = "game/project.godot" ] && { CROSSCUT=1; break; } # autoload 注册面 + for _pat in $PARITY_TOUCH_FACES; do + case "$_f" in + "$_pat"*) CROSSCUT=1; break 2 ;; + esac + done + _t="$(suite_terms_for "$_f")" + [ -n "$_t" ] && _family_terms="$_family_terms $_t" + done <<<"$_codefiles" + # 申报半边:在册需求卡片对照表「验收测试」列的测试文件名(卡片=本批 diff 触及 ∪ roadmap 进行中行登记)。 + _cards="$(git -C "$REPO" -c core.quotepath=false diff --name-only "${_base:-HEAD}" HEAD -- 'reports/' 2>/dev/null | grep '需求卡片.*\.md$' || true)" + _roadmap_cards="$(grep '| 进行中 |' "$REPO/roadmap.md" 2>/dev/null | grep -oE 'reports/需求卡片[^|]*\.md' || true)" + _declared_names="" + _card_seen=0 + for _c in $_cards $_roadmap_cards; do + _card_seen=1 + _cf="$REPO/$_c" + [ -f "$_cf" ] || continue + _names="$(grep -ohE '[a-z0-9_]+_test\.gd' "$_cf" 2>/dev/null | sort -u || true)" + [ -n "$_names" ] && _declared_names="$_declared_names $_names" + done + if [ "$CROSSCUT" -eq 1 ]; then + SCOPE="full" + echo "[gates] 关联档选闸:diff 命中横切面(公共件/数据表/autoload/数值·存档·RNG·结算触面)——02 回落全部套件" + else + # 并集去重:推导族词 ∪ 申报测试文件名;并集非空才过滤(映射表保证族词非空,此处双保险)。 + _union="$(printf '%s\n' $_family_terms $_declared_names | sed '/^$/d' | sort -u | tr '\n' ' ' || true)" + _union="$(echo "$_union" | xargs 2>/dev/null || true)" + if [ -n "$_union" ]; then + SCOPE="related" + ONLY_TERMS="$_union" + else + SCOPE="full" + fi + fi + if [ "$_card_seen" -eq 0 ]; then + echo "[gates] 关联档选闸·申报:未找到在册需求卡片(diff 与 roadmap 进行中行均无),申报集为空——仅按 diff 推导" + fi + if [ "$SCOPE" = related ]; then + echo "[gates] 关联档选闸:02 选套件跑(--only=$ONLY_TERMS)" + fi fi fi @@ -195,11 +324,20 @@ gate() { # gate <名称> <日志名> <命令...> gate_unit() { # run_all 是 --script 模式,退出码为唯一判据 local name="02 单元与集成测试" + # 关联档选跑:把套件词列表经 --only= 传给 run_all(闸机来源标记 --only-source=gates, + # 入口据此打关联档横幅、不落「仅供开发期」免责句);全量态不传任何选跑参数。 + local -a _user_args=() + if [ "$SCOPE" = related ] && [ -n "$ONLY_TERMS" ]; then + _user_args=(-- "--only=${ONLY_TERMS// /,}" --only-source=gates) + fi echo "" echo "════════════════════════════════════════════" echo " $name(--script 模式,退出码为唯一判据)" + if [ "$SCOPE" = related ] && [ -n "$ONLY_TERMS" ]; then + echo " 关联档选跑:--only=$ONLY_TERMS" + fi echo "════════════════════════════════════════════" - ( cd "$REPO" && "$GODOT" --headless --path "$(cygpath -m "$GAME")" --script res://tests/run_all.gd ) \ + ( cd "$REPO" && "$GODOT" --headless --path "$(cygpath -m "$GAME")" --script res://tests/run_all.gd "${_user_args[@]}" ) \ >"$STAMP_DIR/unit.log" 2>&1 local rc=$? # 三硬闸之二(脚本错误扫描):子套件的 SCRIPT ERROR/Parse Error 只写 stderr,入口内收不到, @@ -216,12 +354,8 @@ gate_unit() { # run_all 是 --script 模式,退出码为唯一判据 # ══ 代码域(0–5)══ if in_domain code; then # ── 0. 冒烟测试(极小启动场景:真实开机路径存活检查,零逻辑判定,秒级)── -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 +# 三件保险之一(00/01/09):恒跑不降——测量面批也不免(关联档选闸,2026-10-29 用户裁)。 +gate "00 冒烟测试" "smoke_boot.log" "$PY" "$REPO/tools/run_smoke.py" # ── 1. 系统测试(真实玩法路径断言面;宿主=game/tests/system_test.gd,缺省 --script 由 run_system_test.py 持有)── gate "01 系统测试" "system_test.log" "$PY" "$REPO/tools/run_system_test.py" # ── 2. 单元与集成测试 ── @@ -260,21 +394,10 @@ gate "08e 能力审计" "capability.log" "$PY" "$REPO/tools/capability_audit.py" fi # ── 6c. 回放确定性对拍(条件闸件;属代码域)── if in_domain code; then -# 触面判定:本批 diff 相对 master 分叉点,命中下列路径面之一即「触面」。 +# 触面判定:本批 diff 相对 master 分叉点,命中 PARITY_TOUCH_FACES(脚本上部单点定义)之一即「触面」。 # 触面也**不自动跑**——对拍调的是 MC 脚本,属「MC/统计类跑批发起前须用户批准」红线管辖; # 闸只负责每次跑批都把触面判定做一遍并显式要求批准,杜绝「该跑却没人记得」。 # 用户批准后设 PARITY_APPROVED=1 重跑本脚本即实跑三变体对拍。 -PARITY_TOUCH_FACES=" -game/data/tables/ -game/core/rng_service.gd -game/core/save_manager.gd -game/core/meta_progression.gd -game/core/settlement/ -game/core/damage_math.gd -game/core/damage_controller.gd -game/core/battle_orchestrator.gd -game/sim/adventure_monte_carlo.gd -" _base="$(git -C "$REPO" merge-base master HEAD 2>/dev/null || true)" _changed_files="$(git -C "$REPO" diff --name-only "${_base:-HEAD}" HEAD 2>/dev/null || true)" _touch_hit=0 @@ -315,11 +438,11 @@ else fi # ── 与存量基线比对 ── -"$PY" - "$BASELINE" "$RECEIPT" "$QUICK" "$DOMAIN" "$REPO" "$DIRTY_GAME" "${NAMES[@]}" -- "${EXITS[@]}" -- "${LOGS[@]}" <<'PYEOF' +"$PY" - "$BASELINE" "$RECEIPT" "$QUICK" "$DOMAIN" "$REPO" "$DIRTY_GAME" "$SCOPE" "$ONLY_TERMS" "${NAMES[@]}" -- "${EXITS[@]}" -- "${LOGS[@]}" <<'PYEOF' import hashlib, json, os, subprocess, sys -baseline_path, receipt_path, quick, domain, repo, dirty_game = sys.argv[1:7] -rest = sys.argv[7:] +baseline_path, receipt_path, quick, domain, repo, dirty_game, scope, only_terms = sys.argv[1:9] +rest = sys.argv[9:] sep = rest.index("--") names = rest[:sep] tail = rest[sep + 1:] @@ -414,6 +537,8 @@ with open(receipt_path, "w", encoding="utf-8") as f: "timestamp": subprocess.run(["date", "-Iseconds"], capture_output=True, text=True).stdout.strip(), "quick_mode": quick == "1", "domain": domain, + "scope": scope, + "scope_terms": [t for t in only_terms.split() if t], "dirty_game": dirty_game == "1", "first_baseline_run": first_run, "gates": [{"name": n, "exit": e, "fingerprint": now[n]["fp"]} for n, e in zip(names, exits)], diff --git a/待办清单.md b/待办清单.md index 3ba15bd5..ba1be00a 100644 --- a/待办清单.md +++ b/待办清单.md @@ -2,22 +2,6 @@ 本清单是流水线的唯一登记处:要做的事先在此登记一条,随流水线段推进改状态,做完整条删除;各系统文档的「待办」节只留一行指路存根,不再自建条目。条目格式(标题=登记元数据含可选优先级,正文=实况/缺口/改动面/状态四属性)与状态机以 [doc-conventions §12.1](design/engineering/doc-conventions.md) 为权威。 -#### 合回闸关联档——收口跑批按改动关联面选测(2026-10-29 登记,来源=会话提及) - -**实况**:起因=2026-10-29 迭代 41 收口跑批遇红树空磨(公共件解析错致每套件烧满 120 秒超时,单元段预计 2h+,会话全程静默)。用户裁:合入不再强制全量,改为「修改内容关联的测试都跑通即可」。设计要点(会话已闭合):①收口跑批范围=需求卡片对照表申报的测试族 ∪ 按改动文件机械推导的关联测试族(并集执行,防申报漏标);②三件廉价保险恒跑不降(冒烟/系统测试/静态扫描,合计约 1.5 分钟);③横切面改动(config_db、autoload、存档、RNG、数据表等公共件)映射回落全量;④回执加 scope 字段标注关联档,合回闸校验逻辑不动(树对齐/祖先/域覆盖照验);⑤绿树窄批收口约 3–4 分钟(现全量约 10 分钟)。现行「影响面选闸」(测量面批 SKIP 探针/渲染)是本条推广前的特例。规则同步义务:AGENTS.md「交付前验收」句与 process-conventions §3 合回闸细则同批改写,禁只改闸不改规则。关联条目:「闸机编排快速失败」(本条管跑多宽,彼条管红树磨多久,正交互补)。执行=用户发令;若选闸逻辑下沉 run_all.gd 则含游戏代码面、须走 worktree 批。未决点:「合入后自动补跑一次全量作兜底」待用户裁(折中案=合入前快查+合入后兜底;激进案=合入前只跑关联档)。证据=iter41 worktree 收口跑批现场日志(game/tests/.gates/unit.log 逐套件「超时挂死(120s),进程树已整树回收」记录,2026-10-29),与「迭代 41 测试债修复」条互证。 - -**改动面**:工具代码 - -**状态**:待执行 - -#### 闸机编排快速失败——连续套件超时中止整轮(2026-10-29 登记,来源=会话提及) - -**实况**:单元与集成编排器现行逐套件 120 秒超时回收,红树(如公共件解析错致全部套件挂死)下单元段最长 64×2 分钟≈2h08m,整轮磨完才出红。用户裁加快速失败:连续 N 个套件超时(N 实现期定,3 起步)即中止整轮并汇总已见红因,红树几分钟出结论。run_all.gd 属代码面,走 worktree 批;执行=用户发令。证据=iter41 worktree 收口跑批现场日志(run_all_10→31 逐套件恰以 2 分钟间隔落盘=每套烧满超时,2026-10-29),与「迭代 41 测试债修复」条互证。 - -**改动面**:游戏代码 - -**状态**:待执行 - #### 验证与清单体系整改——四方向分阶段推进(2026-10-28 登记,来源=会话提及) **实况**:用户裁「验证及清单不合理」,经调研定四方向全做、分阶段推进:A 粒度整改(清单从断言级升到能力/行为级,逐断言账本退役)→ B 精简裁断(在新粒度上规则驱动删并)→ C 流程风险分级(P0/P1/P2 验收强度、审计抽样)→ D 文档减肥(待办条目限长、历史归 git);测试有效性度量暂不处理。阶段 0 盘点已毕:验证集清单零机器闸依赖(run_all_gates/hooks/五技能均不引用,仅 test-conventions §8.3 维护锚+2 份过程报告引用),退役无闸改动;但测试名尾缀仅覆盖 28/47 个有断言文件(19 个零尾缀,含 seal_polarity 69 条、variant_placement 42 条、roll_multiplier 35 条),故能力清单只能按「能力→代表用例」建,不要求逐断言尾缀。对照表列改动触 pipeline_gate+req-card/iter 两技能;待办正文压缩不触机检(无长度判据)。盘点明细见 reports/盘点-验证与清单体系-2026-10-28.md。**阶段 1 设计已闭合(2026-10-28 用户四裁点)**:能力清单 v1 落 `game/tests/README.md`——按系统归属与玩家旅程排序,首期核心四域(存档与随机、冒险循环、战斗结算、奖励与经济)29 行+跨域条件闸 1 行,共 30 行,每行真实入口+2–3 个代表用例(`文件:函数`),维护规则为每批收口核对本批受影响行并刷新「最后验证批次」;`design/engineering/test-conventions.md` §8.3 维护锚改写为「验证集清单整册冻结、能力清单承接覆盖索引、阶段 2 精简后删除」;`game/tests/验证集清单.md` 头注同步冻结声明,冻结快照(基点 `b9260138`/1587 条)留作阶段 2 对账基准;`tools/extract_checks.py` 保留为审计工具,不再承担常态同步。**阶段 2 已执行闭合(2026-10-28 C 档合回)**:存量断言精简 C 档 6 组手写断言族并为 7 个表驱动站点(33→7),冻结清单对账 1587→1561,验证集清单随阶段 2 完成退役(覆盖索引归能力清单,维护锚 test-conventions §8.3),删并申报与件核见 reports/工程改动报告-存量断言精简C档-2026-10-28.md;阶段 3–4 待各自设计推进:风险分级与审计抽样规则、条目限长是否入 pipeline_gate 均未定。**2026-10-29 阶段 1 落地定稿(用户三裁)**:①能力清单与覆盖域表落 `game/tests/README.md`,独立成表、不与 GDD 验收标准合并;②GDD 纯度判红——系统与概念文档全文禁写实现细节(代码路径/测试文件名/断言名),唯一例外=配置文件索引(`game/data/tables/*.tres` 表路径与表名);③能力清单维护归写卡段——写需求卡片时增删能力行并刷新「最后验证批次」,实现批禁写该文件(清单按规则滞后一个迭代,属设计后果)。三层模型(验收意图/覆盖索引/执行证据)与四法则落 test-conventions §8.9;落地分五批:①三份规约+AGENTS.md 红线+本条实况(本批);②README 覆盖域表+三域能力行补齐;③`tools/capability_audit.py` 文档域新闸(3a 普通工具入库/3b 接闸+基线,worktree);④pipeline_gate 对照表加「能力行」列+req-card/iter 两技能同步;⑤GDD 存量实现细节清理(28 篇约 98 处,逐处去向表过用户目后落笔)。**2026-10-29 阶段 1 五批全部落地**:①032d923c;②5377b2ce;③`tools/capability_audit.py` 入库 ae3c99e3+接闸上电 915222fa(文档域闸 08e 能力审计,纯度红清零后才上电);④f3263444;⑤GDD 清理 00312e12(30 篇去 124 处实现指证,纯度红 124→0)。阶段 1 收口;阶段 3–4 待各自设计推进。