Compare commits

...
13 Commits
Author SHA1 Message Date
DKChan 7aa0126b22 Merge branch 'glow-freeze'
ci / gates (push) Successful in 34s
2026-11-06 06:19:09 +08:00
DKChan 8d170007be 工程+文档(三关色板终值冻结批): 互辨抽测闭合落文——art-bible v3.43 §6.3 三关两槽终值改指针式冻结(本节不抄色值字面量:终值=各关三块底型瓦件顶面受光档实测最亮值,wall_ambient 同关同槽、零新增色值;凭据句写明样图同哈希故目检面未重跑+§1.4 样张重跑 30/31 唯一红为改前基线缺位属在途素材区环境坑);§4.1 对比硬指标句按用户新立规矩去数值化(「未开格底 L≥0.30/饱和≤0.55/避禁入带」数值子句删,改述底槽-微光-已开格底相对亮度与相对饱和关系+指路执行件登记面);抽测执行件代表值口径订正(H2_VALUES 硬写的换色锚槽表值退役,改由 lit_slots() 从在役底型件像素按线性相对亮度实测;六道门值单点化为 FLOOR_L/CEIL_SAT/BANNED_HUE/NEAR_EPS/B_MIN_CH_DIFF/C_MODEL_TOL,run_checks 内两处内联 8/2 同批收编——本件即这些数值的唯一源);art-pipeline-tools.md 登记该件(选工具表一行+专节含命令/门值单点/坑);reports/读数表补 S3 目检判定表(四条全过)+S4 抽测通过结论;待办清单抽测条整条删除+新登记「美术圣经去数值化」条(状态待文档设计)。
本步凭据: 抽测件实跑 17 项 FAIL 0(dungeon 实测 #B49069 L=0.307 过门余量 0.007/underdark #BAC1D2 0.532/deep_underdark #969EBA 0.345)+样图与 2026-11-05 出图件逐字节同哈希(15d33fabb75256677254…,目检面未变)+§1.4 格子样张重跑 30/31 绿(唯一红 V10=在途素材区无改前基线件,拒用 --save-baseline 造「改前=改后」空对拍)+run_all_gates --docs 闸况与基线一致无新增红(流水线闸 PASS,条目 27;术语闸抓到「不得」两处已按 STC 词表改「禁止」)+目检口径记档(用户回话「没有问题,我觉得是可以分清楚的」,经追问四条覆盖范围答「四条全过」)。

订正备案: 本会话中代理一度判「dungeon 顶面受光档 L=0.235 不达圣经地板」并拟立案,系测量用 sRGB 加权亮度排序选像素、与该门线性域相对亮度口径不同尺度的失误;按件内口径实测 0.307 过门,未据此立任何条。
2026-11-06 06:18:59 +08:00
DKChan f73f3c1d47 合并(QA harness缺陷收口登记): P0条目「QA harness 三批缺陷与覆盖遗漏收口」新立(报告落reports带处置四分流)+统筹条目三批完成整条删除; --docs绿+流水线闸PASS(27条目); pre-merge钩子实弹(前置档读数见本合并输出)
ci / gates (push) Successful in 34s
2026-11-06 05:42:50 +08:00
DKChan a1f1c258ea 登记(QA harness缺陷收口·P0条目+报告落档): 新立「QA harness 三批缺陷与覆盖遗漏收口」P0条目(实况=reports报告指针+处置四分流:A1随camp-interact合回即消/A2·B1·B2走写卡/B3·B6归既有义务/B4·B5人工复核后分流); 报告落reports(八类逐条带文件行号与登记处指针); 统筹条目「QA 覆盖总览面板与自动试玩扩展」三批全落地完成整条删除(完成即删,§12.1)。读数: 流水线闸PASS(27条目)+--docs【验收通过】无新增红(exit=0) 2026-11-06 05:42:40 +08:00
DKChan d3c8539062 合并(QA harness 三批·测试面真输入化+覆盖总览面板+自动试玩扩展): 批1=两探针52处emit站点51处转真输入管线+1接线断言保留+probe_coverage_recorder覆盖登记件(锚链归一化键)+§2条文+滚轮press-release成对/折叠区滚入视野两实证规范; 批2=B覆盖总览面板四节(六层闸况/能力行×六层灯/控件下钻/本体对账)+义务层提取器7段103行+六源回执明细化闸壳挂点+本体枚举器865实体对账+豁免清单母版+登记件seen面; 批3=journey_player全动线机器自玩(16类屏到达/异常三类含归因通道/误报指纹跨跑静默实测生效/真输入命中30控件并入登记)。三批读数: 批1探针孤立直跑绿+全量档绿; 批2全量档绿+面板浏览器真实入口四节验过; 批3首跑机器复现在案缺陷「开局被开始层盖住」(营地交互修复批承接中)+二跑收敛PASS exit=0。方案偏差20条回写qa-harness讨论文档§10-§12; 三处环境/机制堵坑(.gd行尾LF/全局类缓存扫描/夹具清槽+排版帧)。合成并master c1acb47c(pre-merge钩子批)——本合并即该钩子第二次实弹
ci / gates (push) Successful in 7m15s
2026-11-06 05:28:00 +08:00
DKChan be0cab8356 Merge branch 'master' into qa-harness
# Conflicts:
#	.gitattributes
2026-11-06 05:24:31 +08:00
DKChan d7d4bc87d3 批3(QA harness·C自动试玩扩展): 新增game/sim/journey_player.gd+.tscn全动线机器自玩驱动件(开机→设置→存档三操作新档/删档/进档→营地五窗逐窗→Esc回退→大门开冒险→关卡推进→战斗→停点分发→终局收束→回菜单); 战斗策略委托既有auto_player,按钮/物件全真输入管线(ProbeUiUtil单点),两方法口如实标通道(地图候选=press_candidate自绘布局命中,棋盘格=click_cell BoardView公共口——棋盘管线面归ui_input_probe); 异常三类上报含机器自填归因初判+通道标签,fatal=诊断收集器级(battle_result=fatal_error),段间超时30s+战斗状态挂死90帧,屏互斥六对检查; 误报指纹ledger跨跑持久(同源第2次自动静默); 报告journey_report.js+命中30控件并入覆盖登记。读数: 首跑16类屏到达/上报2=机器首跑即复现在案缺陷「开局被开始层盖住」(营地交互修复批承接中——开始层可见直入locks_input谓词锁死战斗,C上报后夹具面隐藏继续走查,缺陷本体不掩盖); 二跑上报0/静默2/PASS exit=0(收敛机制实测生效); 冒险段0 fatal。实施期自修两坑: 陈旧槽文件夹具卫生(段首清槽)+开层排版帧(选档钮/面板继续钮同帧点击落空——选档空转假红与输入锁滞留两形态)。方案偏差6条回写qa-harness讨论文档§12 2026-11-06 05:23:29 +08:00
DKChan 8d8e4255c3 结转(QA harness批2·统筹条目推进): 进度①②已落地(批2=B面板四节+义务层提取器+六源回执明细化闸壳挂点+本体枚举器865实体对账,提交bb9ce8a1),③待批2验收后开工; 方案偏差6条回写qa-harness讨论文档§11(执行层挂点取编排器末尾单点/probe_manifest机制/本体缺口×探针命中交叉降级/义务层匹配口径/豁免清单落点/登记件seen面)。读数: 流水线闸PASS(27条目) 2026-11-06 03:22:09 +08:00
DKChan bb9ce8a117 批2(QA harness·B覆盖总览面板+本体枚举器): 义务层=tools/extract_capability_list.py(README能力清单四列表按旅程段解析,7段103行,用例指针剥反引号); 执行层=tools/coverage_receipts.py六源回执明细化(冒烟/单元集成/系统/探针/渲染/静态各一.js,名×结果×计数粒度,日志缺失=not_run亮灰不冒充绿)+run_all_gates.sh末尾挂点(观测件非判定件,解析失败打红字不翻闸判)+probe_manifest.js(面板file://无法列目录,按现存登记文件落清单); 机器义务双源之本体源=tools/coverage_enum.py(两扫描器+比对:.tres行集/schema词表字段/tscn按钮/gui_input自绘类/屏模态清单×extract_checks断言文料,865实体/缺口212); 呈现层=tools/coverage_report.html(四节:六层闸况/能力行×六层灯按旅程段/控件下钻交互源+渲染golden展示源/本体对账缺口×探针命中降级探针承载;状态灯四态+数据时间戳陈旧可见+完备性边界页眉;JSONP直载file://即开); 豁免=tools/coverage_exemptions.js人工维护母版; 登记件扩seen面(场景树全量BaseButton+db布局表hotspot_key公共面枚举,seen-hit=存在但零真输入覆盖缺口数据源)。读数: 两探针窄跑绿(screen_flow 19命中/116存在,ui_click 27命中/91存在——停手钮/树购买重建代等真缺口在册,弃图刷新确认链走视图方法属衔接条目余项); 六源明细化对真实闸日志抽核(unit82套件/系统69检查/探针10段/静态13条/golden逐屏L1-L3); 面板浏览器真实入口四节渲染验过; 全量档闸【验收通过】无新增红(exit=0,回执commit 07d4977f,六源明细化在闸内实跑=挂点真路径验证) 2026-11-06 03:21:10 +08:00
DKChan 07d4977f41 结转(QA harness批1·待办清单两条推进): 统筹条目实况补批1落地凭据(worktree qa-harness·两探针51处真输入化+1接线断言保留+覆盖登记件+§2条文,偏差八条回写方案文档§10),①改已落地②③待验收; 衔接条目已承接面改两探针52处站点(51转真输入+1标接线断言),剩余面收窄为视图层测试方法面+996条精锚。读数: 全量档闸【验收通过】闸况与基线一致无新增红(exit=0,回执commit 0ec53f42——含单元与集成4/4分片绿·场景探针族绿·渲染闸绿·静态扫描·文档域·熵增闸); 本批终态=qa-harness分支四笔(2cbd2ca9批1实现/88636868并master 1730a8fb/afce55b0 gd行尾属性/本笔) 2026-11-06 02:24:50 +08:00
DKChan afce55b09f 工程(.gd行尾属性·批1闸机堵坑): .gitattributes补*.gd text eol=lf——CRLF检出×套件内嵌r"""golden字面量逐位对拍吃进\r即假红(实证:settlement_wiring_snapshot_test在本worktree与主区双双1/3失败,同一字节Mac CI检出LF为绿),对齐tools/hooks例外同款; rm+重检出全区gd转LF零内容变化。读数: 导入扫描刷全局类缓存后LevelMapViewport入册(fog/forge两套件解析红清),pacing_queue挂死系缓存级联复跑绿(主区对照exit=0),快照套件修复后exit=0(封装5.1逐位对拍PASS) 2026-11-06 02:13:00 +08:00
DKChan 886368684c Merge branch 'master' into qa-harness
# Conflicts:
#	game/probe/probe_ui_util.gd
2026-11-06 01:52:17 +08:00
DKChan 2cbd2ca9a7 批1(QA harness·探针存量emit真输入化+覆盖登记写入): 两探针52处emit站点转真输入管线(push_input press+release成对)51处,保留1处置灰态穿透emit标〔接线断言〕; 新增probe_coverage_recorder.gd真输入覆盖登记件(旗标在位期间命中入账,锚链归一化键=具名/脚本锚+类#钮面文本+序位,@临时名不入键,JSONP落tests/.gates/coverage/每探针一件); probe_ui_util补coverage_hook单点+push_motion/push_wheel(滚轮press+release成对——只发press钉死鼠标焦点实证fork/返回营地两钮点击全失效)/window_panel/scroll_button_into_view(折叠区行钮rect越窗底栏实证误中返回营地钮,真输入滚轮滚入可视区再点); V5b遮挡试探订正=前窗经真实返回钮清账再真点开窗(emit直发穿遮罩属口径违例); 落点判定补钉2处(esc段子界面开窗落点/树窗返回钮落点); test-conventions §2新增UI驱动断言真输入管线条文(滚轮成对+折叠区滚入+落点判定+接线断言豁免口径); .gates/.gitignore收coverage/。读数: screen_flow_probe孤立直跑PASS(六步动线+屏级互斥全绿); ui_click_probe孤立直跑exit=0问题=0(budget=15210,三笔树购买请求全通,PROBE_SEED=7); 登记落盘ui_click=27控件/38命中/临时名跳过0,screen_flow=19控件/25命中。全量档闸(fd9c8ad9基线): 两新增红均判为主区推进漂移非本批引入——流水线闸红=待办清单QA条目证据句(master 1730a8fb已修),单元settlement_wiring_snapshot红待并master后窄跑复核; 本步凭据=两探针直跑绿+偏差8条已回写qa-harness讨论文档§10 2026-11-06 01:50:50 +08:00
21 changed files with 1912 additions and 134 deletions

No files matched your search

+3 -3
View File
@@ -1,6 +1,6 @@
# 美术圣经(Art Bible)— Dungeon Sweeper
> 项目:Dungeon Sweeper(扫雷 × 自动战斗 闯关 Roguelike)· 状态:**第 1–9 节定稿**;现行为 v3.42,历次修订明细见 git 提交史
> 项目:Dungeon Sweeper(扫雷 × 自动战斗 闯关 Roguelike)· 状态:**第 1–9 节定稿**;现行为 v3.43,历次修订明细见 git 提交史
>
> **Art Director Sign-Off(AD-ART-BIBLE)**:历次批均未签核——此为 Lean/Solo 模式(非 PHASE-GATE)下的现行治理状态;full 门禁签核时机待用户表态。
> 风格基调:地牢题材但**非暗黑风**——轻快明亮的幻想冒险风,全年龄;像素风;角色 2~3 头身。
@@ -215,7 +215,7 @@
- **换色合并登记五处(用户裁,部件色落值)**|按本表「不过门即触发换色合并、禁加色绕行」执行,五处**零新增额度**(各件原拟新增候选一律不取):① 主角铲刃浅铁灰(原拟 `#A8AEB8`)→ 并入 `FOG_MEMORY_INK #C4C4C4`,**部件取值 = `#C4C4C4`**;② 主角提灯铜色(原拟 `#C08A3A`)→ 并入 `GOLD_DEEP #B07A14`,**部件取值 = `#B07A14`**;③ 食人魔酋长暖褐皮(原拟 `#B08050` / `#A97C4E`)→ 并入 `暖石 #B8A582`,**部件取值 = `#B8A582`**;④ 石君王灰皮(原拟 `#9A968C` / `#949089`,按**次级皮**口径——板甲视同体块主色、灰皮属次级皮故不受 L 0.22–0.45 约束)→ 并入 `STONE #786E62`,**部件取值 = `#786E62`**;⑤ 轻甲护甲图标木色 `#8A5A34`(深素底下 2.72:1 不过门;原拟新增木色亮档 `#C49A5E`)→ 并入 `WARM_FAINT #CDB88C`,**部件取值 = `#CDB88C`**(8.08:1)。**一色两岗代价已随裁定接受**:① 的 `#C4C4C4` 语义岗 = 迷雾记忆墨(§6.5)、② 的 `#B07A14` 属金族(§4.2 金只说收益与兑现)——两者均作角色随身件材质色用,不进信息层与语义物件岗;③④⑤ 无语义岗冲突。验收:机检〈五处部件取值点全部改指合并目标色,四枚原拟新增候选值零入板〉
- **换色合并登记三处**|按本表「不过门即触发换色合并、禁加色绕行」执行,三处零新增额度:①地台顶缘受光线 `#C4AC84` 已并入 `#C9AF83`(掩尘枚 `BRICK`,与 §8.2 高墙口沿高光同枚)——源常量已删除(全仓零引用,退役断言测试串为预期例外),绘制点全部改指 `#C9AF83`;②地台描边 `#5A4C3A` 已并入 `#4C4038`(代码 `PLAZA_EDGE`,ui_kit 唯一落点)——源常量已删除,绘制点全部改指;③舞台底 `#3E342A`(代码 `STAGE_BG`)→ 并入 `#3E362C`(状态槽底)——源常量已删除(常量表零键),背景资产接管该绘制面,程序化平色缺图回退改指 `SLOT_BACKDROP`(`#3E362C`)。程序化回退绘制与已落合并值的贴图保持一致。验收:机检〈①②两枚源常量全仓零引用,绘制点全部改指合并目标色〉
**对比硬指标(可测)**:数字可读性由**格底明度分档**承载——棋盘数字亮墨 **L≥0.75** 对**已开格底 L 0.08–0.14** ⇒ 直画对比 **≥4.5:1 恒成立**(WCAG AA,样张逐组合抽测冻结);**色牌后备**:色牌素色恒定 **L≥0.70**、牌上数字深墨 **L≤0.027** ⇒ 任意牌墨组合 ≥4.5:1(实测样例:#33261B/#F5EAD3 = 12.3:1)。未开格底槽(含全部图类型)相对亮度 **L≥0.30**(墙顶面受光档)、HSV 饱和 **≤0.55**、避 Boss 禁入色相带;已开格底走世界底层档(见上)。物件识别不依赖格底(§5.1 轮廓通道),封印印记、金色不作识别依赖。验收:机检〈亮墨对已开格底 ≥4.5:1;未开格底 L≥0.30、饱和 ≤0.55;任意牌墨组合 ≥4.5:1〉 ←原二;地形变异 AC-9
**对比硬指标(可测)**:数字可读性由**格底明度分档**承载——棋盘数字亮墨 **L≥0.75** 对**已开格底 L 0.08–0.14** ⇒ 直画对比 **≥4.5:1 恒成立**(WCAG AA,样张逐组合抽测冻结);**色牌后备**:色牌素色恒定 **L≥0.70**、牌上数字深墨 **L≤0.027** ⇒ 任意牌墨组合 ≥4.5:1(实测样例:#33261B/#F5EAD3 = 12.3:1)。未开格底槽(含全部图类型)与类型微光、已开格底之间存在**相对亮度与相对饱和关系**——禁止底槽吞没微光环、禁止底槽与已开格底同档;本节只定这层关系,阈值与准入判定由图类型色板互辨抽测的执行件承载(用法登记见 [art-pipeline-tools](art-pipeline-tools.md))。已开格底走世界底层档(见上)。物件识别不依赖格底(§5.1 轮廓通道),封印印记、金色不作识别依赖。验收:机检〈亮墨对已开格底 ≥4.5:1;未开格底对微光环与对已开格底的相对亮度关系经互辨抽测准入;任意牌墨组合 ≥4.5:1〉 ←原二;地形变异 AC-9
**色板增量纪律(数值状态 = 待定现用)**:全作色板硬上限 **48 色**(防色板无限膨胀的封口条款;**额度已用尽、余量 0**,后续余量以色板检查器实报为准);新色入板须同时过两道门:①§4.2 语义检查(不占语义色段/禁入带);②**与既有任一色的 RGB 三通道差禁止全部 <8/255**——即三通道里至少一通道 ≥8/255 即过门,「三通道差全部 <8」才算不过门(条文原写的「三通道差 ≥8/255」有「逐通道全过」与「禁止全灭」两种互斥读法,前者下现行色板 247 对不达标、与本节自认的「现行即执法」合规状态不能同真,故取后者;用户裁)。**额度已用尽:后续任何新增须先压缩或合并既有色**——按本节「额度不足时呈报作者」口径呈报,**压缩或合并哪几枚由作者裁,本节不代裁、不预指对象**。反例实证:#3A2E22 ↔ #362B20 三通道差均 <5,肉眼不可辨,同板共存只增噪。不过门即触发「换色合并」(把意图色并进最近既有色),**禁加色绕行**;加色义务同时登记于本表修订。**「待定现用」态释义**:数值**现行即执法**(色板检查器按此拦,§8.3)——非候选(不存在"等冻结"空窗)、非死规(后续需求引入时可变);调整走本节修订并登记,改值 = 修订不是违约。验收:机检〈色板总数 ≤48;新色入板过两道门〉
@@ -401,7 +401,7 @@
### 6.3 三关图类型色板候选(board_type 换挡方向)
- **模板与不变槽**:换皮只换 §4.3 四槽(`tile_closed`/`tile_open`/`wall_ambient`/`variant_tint`),信息与语义层全图类型恒定——本节只给三关**方向性候选**,终值按 §4.3 现行规则重跑 §1.4 格子样张冻结。**终值的取值程序**:先跑「三关色相 × 五组类型微光」互辨抽测(抽测任务登记在待办清单),再按 §4.3 现行规则重跑 §1.4 格子样张冻结——本节不填终值色值,待该抽测与格子样张重跑后落本节修订。验收:机检〈本节三关行无 `tile_closed`/`wall_ambient` 终值色值字面量(终值未冻结前禁填)〉
- **模板与不变槽**:换皮只换 §4.3 四槽(`tile_closed`/`tile_open`/`wall_ambient`/`variant_tint`),信息与语义层全图类型恒定——本节的三关两槽终值**已冻结**。**冻结口径(本节不抄色值字面量)**:各关 `tile_closed` 终值=该关在役墙瓦片三块底型件(素砖/裂纹/碎石斑)顶面受光档的实测最亮值,`wall_ambient` 取同关同槽值(两槽同值;同属 §4.1 分层②场景环境子板,零新增色值、不占 48 额度);实测值与准入判定只在执行件里算,执行件用法登记见 [art-pipeline-tools](art-pipeline-tools.md)。**冻结凭据**=「三关色相 × 五组类型微光」互辨抽测闭合(机检 17 项 0 红+目检四条全过;抽测样图与 2026-11-05 出图件同哈希,目检面未重跑)+ §1.4 格子样张重跑机检 30/31 绿(唯一红灯=改前/改后对拍一件,因在途素材区无改前基线件、属素材区环境坑不属色板回归,沿革查 git 提交史)——砖面形态与色板维持冻结。验收:机检〈三关行两槽终值与互辨抽测执行件的实测读数同值(该件三行铺底代表值判定全过),且本节三关行不含色值字面量〉
- **三关背景变体**:§6.1 的三张背景取本节三关方向作为岩体形态、剪影语言与色族差异(dungeon 暖石棕+地精巢穴支洞/木栈道/木桩围栏;underdark 冷蓝灰+卓尔巨柱/石笋/蛛网/螺旋塔剪影/巨菇/石桥;deep_underdark 灰黑+灰矮人石笋城郭/棱角石厅/矿道口/烟囱塔);背景不复用 `wall_ambient` tint(该槽作用于棋盘/世界),背景色值归战斗屏场景环境子板。验收:目检〈三关背景可辨为不同洞窟,非同一图换色〉
- **深度轴叙事**(§2.7 下探过渡):色相层进预告「更深更险」,明度档不动——
- **dungeon 地精洞窟**(第一关):现行掩尘系暖石棕(`#C9AF83` 基)即基准档,`wall_ambient` 暖棕;
+20
View File
@@ -15,6 +15,7 @@
| UI 框材(面板、按钮、石框、底噪、图标) | `tools/quantize_ui_skin.py` | `tools/palette_check.py`、`tools/path_check.py` | 主题小件另跑 `tools/gen_theme_icons.py` |
| 地形瓦片 | `tools/recolor_terrain_tiles.py` | `tools/palette_check.py` | 只换色,不重绘 |
| 整屏样张 | `tools/render_sample_sheet_v3.py`、`tools/render_level_map_sheet.py` | `tools/shot_checker.py`;棋盘回归用 `tools/golden_board_crop.py` 加 `tools/golden_compare.py` | 样张脚本生成后自带机检 |
| 图类型色板互辨抽测(三关格底 × 五组微光) | `tools/glow_discrimination_sheet.py` | `tools/palette_check.py`(近色比对池同源)、`tools/shot_checker.py`(WCAG 公式同源) | 出并列样图+逐对读数表;格底准入的门值单点在本件 |
## 三条已知边界
@@ -291,3 +292,22 @@ python tools/path_check.py --repo . --code game
**判据或口径**:L1 查 `asset-path-map` §1 的声明目录,`design/art/candidates` 按需创建豁免。L2 查 `game/assets/art` 的四个语义子目录与现役文件清单,含主角 idle/walk/portrait、八敌、铲三帧、封印印记。L3 grep 代码里的 `res://assets/art` 引用,核对盘上存在性,并检出 `board_floor`、`board_wall`、`pillar_dungeon`、`production/mockups` 等退役命名;路径模板含 `%s` 或 `%d` 的跳过。worktree 素材未同步时,缺失项标 SKIP 不判红;主区完整素材环境才全量判。全过 0,有红灯 1。
**已知坑与适用边界**:依赖 `asset-path-map.md` 的声明清单与代码常量;新增素材目录不更新文档就不会被查。素材不入 git,worktree 空或半同步会大量 SKIP,不能据此判断主区健康。只核对路径存在与退役命名,不核对图像内容、尺寸或色板。
## tools/glow_discrimination_sheet.py
**一句话定位**:三关图类型色板 × 五组类型微光的互辨抽测执行件——出一张并列样图(54 格=三关 × 三底型 × 五组 + 基准格,含灰度副本段与图例),自带机检判定并落逐对读数表。
**归属**:仓库内 `tools/glow_discrimination_sheet.py`。
**输入与输出**:读 `game/assets/art/board/terrain_wall_center*_ <board_type>.png` 三块底型件、`game/presentation/views/ui_kit.gd` 的微光冻结常量、`game/tests/golden/palette_baseline/` 两张条带作近色比对池;合成侧逐像素复刻 `game/presentation/views/cell_type_glow.gdshader`。产物=`artwork/mockups/glow-discrimination-sheet-<日期>.png`(不入 git)+ `reports/` 下当日读数表。
**用法命令**:
```bash
python tools/glow_discrimination_sheet.py # 取准入读数 + 出样图 + 机检全道 + 落读数表
python tools/glow_discrimination_sheet.py --no-sheet # 只跑铺底准入五门,不出图不落表
```
**判据或口径**:门值单点在本件常量,文档侧不抄——`FLOOR_L`(未开格底线性亮度地板)、`CEIL_SAT`(饱和上限)、`BANNED_HUE`(禁入色相带)、`NEAR_EPS`(近色容差)、`B_MIN_CH_DIFF`(格底对微光可辨底线)、`C_MODEL_TOL`(样图与合成模型容差)。铺底代表值不查色表:取该关三块底型件像素里**线性相对亮度最大**的实测色(顶面受光档),素材换色后读数自动跟随。判定四道=A 五组明度阶梯单调且组间对比过线、B 格底对微光的三通道差禁止全部小于容差、C 样图与合成模型差落在容差内、D 准入五门;目检四条归用户,与机器判定并列取严(任一侧不过即该对不过)。退出码 0=全绿、1=有 FAIL、2=底型件缺位硬阻塞。
**已知坑与适用边界**:素材不入 git,worktree 缺三块底型件时直接 `exit(2)`——抽测的先决条件不成立,不是假红。色相吞没刻意不设机检阈值:本仓无一条文给出色相差门槛,自造一个等于把观感问题伪装成可机检问题,故色相差只进读数表列、不参与判定。带域与格心取样口径与 `game/probe/type_glow_shot_probe.gd` 同源(带域 de<格边×0.125、格心内缩 20%),改探针口径须同步本件。
+2
View File
@@ -20,6 +20,8 @@
凡模态覆盖层,探针必须断言:打开后自身 visible 且遮挡物已隐藏(模态被遮挡 = 功能存在但不可达)。
**UI 驱动断言须走真输入管线(2026-11-06 立)**:探针里「点按钮/点场景物件」类驱动断言,一律经引擎输入管线承载(合成 InputEvent 走 `push_input`,press+release 成对;合成滚轮同样必须 press+release 成对——只发 press 会把鼠标焦点钉死在按下点控件上,后续点击全部失效);信号直发(`pressed.emit()` 等)绕过命中测试,保不了「玩家点击可达」,只许用于真输入到不了的面并当场标〔接线断言〕(现役例:置灰态穿透钉处理器兜底)。折叠区控件先以真输入滚轮滚入可视区再点。每个动线断言钉「落点」判定(点了之后到哪个屏/哪层可见),防驱动中断后的空转假绿。真输入命中控件时自动登记控件覆盖(探针共用面登记件落 `.gates/` 观测产物),覆盖面生长不靠人记。
## 3. 缺陷修复必须沉淀回归断言
每个缺陷修复必须在探针或冒烟中留下能抓住该缺陷的断言(例:封印动作耗尽 AP → 必须进入第 2 轮;
+242
View File
@@ -0,0 +1,242 @@
# probe_coverage_recorder.gd — 真输入覆盖登记件(探针共用面)
# 是什么:探针/C 驱动件把合成 InputEvent 经 Viewport.push_input 送进引擎输入管线时,登记
# 「真输入实际命中了哪些交互控件」,探针收尾落 JSONP 式 .js 到 tests/.gates/coverage/
# (.js 供 file:// 页面 <script src> 直载——fetch 本地 json 会被 CORS 拦)。
# 成员面(两源):① 场景树全部 BaseButton——pressed 沿自动接线(重扫懒触发,见下);
# ② 自绘命中类——现役接 camp_scene_view.object_pressed(信号带物件 id,key 追加 #物件id 后缀),
# 其余自绘命中面走 note_hit() 显式口。
# 真输入旗标:push 前置位、帧末清位——只有旗标在位期间的命中入账;信号直发(〔接线断言〕保留站)
# 与产品码自发射永不入账。命中=控件真的收到事件:hitbox 错位/遮挡时零命中,缺口自然暴露——
# 这是「新控件落地→覆盖登记自动出现它」的数据源。
# key 规则(契约「场景文件路径+节点路径」的实况落地偏差,详见 qa-harness 讨论文档回写节):
# 本仓库 UI 大头由代码构建(main_ui_build/ui_kit 不设节点名),运行时节点路径全为 @类@序
# 临时名——契约原样落地产出空登记。故键取「锚链归一化」形态:自主根到控件只收集带稳定身份的
# 锚(具名节点用其名、带脚本节点用脚本文件名;@ 临时名段整体不入键),控件本体记 类#钮面文本,
# 同键控件按接线遍历序追加 #序位。键在确定构建序下可复现;UI 构建序变更会平移同键序位——
# 表现为键漂移而非静默丢失,属测量面可接受代价。
# 用法:探针 _ready 里 `_recorder = ProbeCoverageRecorder.new()` → `attach(main, "<探针名>")`;
# 收尾 `dump_js()`。探针共用面不入 S 系扫描面(code-conventions §4.13 分档)。
extends RefCounted
const ProbeUiUtil := preload("res://probe/probe_ui_util.gd")
var _main: Node
var _probe_name: String
var _pending := false # 真输入在途旗标
var _camp: Control = null # 营地场景根(自绘命中单点)
var _hits: Dictionary = {} # 稳定 key(String) → 命中次数(int)
var _node_keys: Dictionary = {} # 接线时分配的节点键(instance_id → key)
var _key_seen: Dictionary = {} # 基键出现次数(同键控件按遍历序追加序位)
var _wired: Dictionary = {} # 已连 pressed 的按钮实例 id → true(防重连)
var _skipped_temp := 0 # 临时名命中笔数(不入 key,只计数)
# ---- 装载与接线 ----
func attach(main: Node, probe_name: String) -> void:
_main = main
_probe_name = probe_name
ProbeUiUtil.coverage_hook = self # 共用件真输入口统一过旗标(push_click/push_click_at/push_motion)
_wire_buttons()
_wire_camp_scene()
# 懒重扫:每次真输入 push 前接线一遍新入树的按钮(开层重建的钮行在下次点击前接上);
# 已释放按钮的连接随节点释放失效,登记表按 instance_id 清扫。
func mark_real_input() -> void:
for id in _wired.keys():
if instance_from_id(int(id)) == null:
_wired.erase(id)
_node_keys.erase(id)
_wire_buttons()
if not _pending: # 同帧多次 push 只挂一次帧末清位旗(重复 connect 会报引擎错)
_pending = true
_main.get_tree().process_frame.connect(_clear_pending, CONNECT_ONE_SHOT)
func _clear_pending() -> void:
_pending = false
func _wire_buttons() -> void:
_wire_into(_main)
func _wire_into(node: Node) -> void:
if node is BaseButton:
var id := node.get_instance_id()
if not _wired.has(id):
_wired[id] = true
_node_keys[id] = _assign_key(node)
(node as BaseButton).pressed.connect(_on_button_pressed.bind(id))
for child in node.get_children():
_wire_into(child)
func _wire_camp_scene() -> void:
_camp = _find_by_script(_main, "camp_scene_view")
if _camp != null:
_camp.object_pressed.connect(_on_camp_object)
func _find_by_script(root: Node, script_name: String) -> Control:
var s: Script = root.get_script()
if s != null and String(s.resource_path).contains(script_name) and root is Control:
return root as Control
for child in root.get_children():
var found := _find_by_script(child, script_name)
if found != null:
return found
return null
# ---- 键分配(锚链归一化,接线时一次算好存表)----
func _assign_key(n: Node) -> String:
var chain := _anchor_chain(n)
var leaf := ""
if n is BaseButton:
leaf = "%s#%s" % [n.get_class(), String((n as BaseButton).text)]
else:
leaf = n.get_class()
var base := "%s:%s/%s" % [String(_main.scene_file_path), "/".join(chain), leaf]
# 同键控件按接线遍历序加序位(#1 省略)——重复键可分账,序位随构建序漂移属已知代价
_key_seen[base] = int(_key_seen.get(base, 0)) + 1
if int(_key_seen[base]) > 1:
base += "#%d" % int(_key_seen[base])
return base
# 自主根到 n 的稳定锚链:具名节点取其名、带脚本节点取脚本文件名;@ 临时名段不入链。
func _anchor_chain(n: Node) -> Array[String]:
var chain: Array[String] = []
var cur: Node = n
while cur != null and cur != _main:
var nm := String(cur.name)
if not nm.begins_with("@"):
chain.push_front(nm)
elif cur.get_script() != null:
var sp := String((cur.get_script() as Script).resource_path)
chain.push_front(sp.get_file().trim_suffix(".gd"))
cur = cur.get_parent()
if chain.is_empty():
chain.push_front("(root)")
return chain
# ---- 显式登记口(自绘命中类无信号可连时的单点)----
func note_hit(n: Node, suffix: String = "") -> void:
if not _pending or n == null:
return
var id := n.get_instance_id()
var key := String(_node_keys.get(id, ""))
if key == "":
key = _assign_key(n)
_node_keys[id] = key
_bump(key + suffix)
# ---- 命中处理 ----
func _on_button_pressed(id: int) -> void:
if not _pending:
return
var key := String(_node_keys.get(id, ""))
if key == "":
_skipped_temp += 1
return
_bump(key)
func _on_camp_object(key: StringName) -> void:
if not _pending or _camp == null:
return
var id := _camp.get_instance_id()
var base := String(_node_keys.get(id, ""))
if base == "":
base = _assign_key(_camp)
_node_keys[id] = base
_bump(base + "#" + String(key))
func _bump(key: String) -> void:
_hits[key] = int(_hits.get(key, 0)) + 1
# ---- 场景树全量交互面枚举(控件缺口数据源:seen−hit=「存在但零真输入覆盖」)----
# 成员面与登记同源:场景树全部 BaseButton(main 建树常驻——未开过的屏的钮也算存在;
# 已接线钮复用接线键防序位错位,未接线者落盘期才补键——落盘是终态动作,序位计数此后不再消费)
# +营地物件(经 db 布局表 hotspot_key 公共数据面枚举;物件命中见 _on_camp_object 的 #物件id 后缀)。
func enumerate_seen() -> Array[String]:
var keys: Array[String] = []
_seen_into(_main, keys)
var db = _main.get("db")
if db != null and db.get("presentation") != null:
var layout = db.presentation.camp_scene_layout
if layout != null:
for spec in layout.layers:
var hk: StringName = spec.hotspot_key
var ck := "camp_object#" + String(hk)
if hk != &"" and not keys.has(ck):
keys.append(ck)
return keys
func _seen_into(node: Node, keys: Array[String]) -> void:
if node is BaseButton:
var id := node.get_instance_id()
var key := String(_node_keys.get(id, ""))
if key == "":
key = _assign_key(node)
_node_keys[id] = key
if not keys.has(key):
keys.append(key)
for child in node.get_children():
_seen_into(child, keys)
# ---- 落盘(JSONP 式 .js——file:// 页面 <script src> 直载)----
func dump_js() -> void:
var dir := DirAccess.open("res://")
if dir == null:
push_error("[coverage] res:// 打不开,登记未落盘")
return
var mk := dir.make_dir_recursive("tests/.gates/coverage")
if mk != OK and not dir.dir_exists("tests/.gates/coverage"):
push_error("[coverage] coverage 目录建不了(err=%d),登记未落盘" % mk)
return
var path := "res://tests/.gates/coverage/probe_hits_%s.js" % _probe_name
var f := FileAccess.open(path, FileAccess.WRITE)
if f == null:
push_error("[coverage] 登记文件打不开:%s" % path)
return
var total := 0
for k in _hits:
total += int(_hits[k])
var seen := enumerate_seen()
var payload := {
"meta": {
"probe": _probe_name,
"seed": OS.get_environment("PROBE_SEED"),
"scene": String(_main.scene_file_path),
"generated_at": Time.get_datetime_string_from_system(),
"driver": "InputEvent via Viewport.push_input(真输入管线)",
"key_shape": "锚链归一化(具名/脚本锚+类#文本+序位;@ 临时名不入键)",
"hits_total": total,
"seen_total": seen.size(),
"skipped_temp_names": _skipped_temp,
},
"hits": _hits,
"seen": seen,
}
var js := "/* 真输入覆盖登记——探针每次运行自动覆写,人工修订无效(豁免面走人工清单,不落本文件)。 */\n"
js += "window.__QA_COVERAGE__ = window.__QA_COVERAGE__ || {};\n"
js += "window.__QA_COVERAGE__.probe_real_input = window.__QA_COVERAGE__.probe_real_input || {};\n"
js += "window.__QA_COVERAGE__.probe_real_input[%s] = %s;\n" % [
JSON.stringify(_probe_name), JSON.stringify(payload, " ")]
f.store_string(js)
f.close()
print("[coverage] 真输入命中登记已落盘 %s(控件 %d 处 / 命中 %d 次 / 临时名跳过 %d)" % [
path, _hits.size(), total, _skipped_temp])
@@ -0,0 +1 @@
uid://d1uhtlw3kmhxw
+80 -2
View File
@@ -120,13 +120,20 @@ static func isolate_save(main, tag: String, mark_first_done: bool = false, with_
# ---- 真输入点击承载(test-conventions §8.3「真实入口」判定口径)----
# 合成鼠标事件经 Viewport.push_input 进引擎输入管线(hitbox/命中测试参与),press+release 成对
#(BaseButton pressed 信号走 release 沿——ui_input_probe :717-733 / screen_flow_probe :484-499 同式)。
#(BaseButton pressed 信号走 release 沿——ui_input_probe :717-733 先例式)。
# 信号直发(pressed.emit())只许标「接线验证」,不得充当真实入口断言的驱动。
# coverage_hook = 真输入覆盖登记件(probe_coverage_recorder.gd)挂点:本件三个 push 口是探针
# 真输入的单点通道,push 前通知登记件置旗标——命中记账只认旗标在位期间(信号直发不入账)。
static var coverage_hook: Object = null
static func push_click(main, ctrl: Control) -> void:
push_click_at(main, ctrl.get_global_rect().get_center())
static func push_click_at(main, pos: Vector2) -> void:
if coverage_hook != null:
coverage_hook.mark_real_input()
var down := InputEventMouseButton.new()
down.position = pos
down.global_position = pos
@@ -141,6 +148,71 @@ static func push_click_at(main, pos: Vector2) -> void:
main.get_viewport().push_input(up, true)
# 悬停真输入(tooltip/悬停态断言的承载——motion 进管线触发 mouse_entered/exited 沿)
static func push_motion(main, pos: Vector2) -> void:
if coverage_hook != null:
coverage_hook.mark_real_input()
var mv := InputEventMouseMotion.new()
mv.position = pos
mv.global_position = pos
main.get_viewport().push_input(mv, true)
# 滚轮真输入(dir=+1 向下翻 / -1 向上翻——折叠区行滚入视野的唯一真输入通道)。
# wheel 合成必须 press+release 成对:硬件滚轮虽无 release 沿,但引擎 GUI 会把按键事件的
# press 记为鼠标焦点且等 release 才释放——只发 press 会把焦点钉死在按下点控件上,
# 后续左键点击全被路由给焦点控件(实证:滚轮后 fork/返回营地两钮点击全部失效)。
static func push_wheel(main, pos: Vector2, dir: int) -> void:
if coverage_hook != null:
coverage_hook.mark_real_input()
var idx := MOUSE_BUTTON_WHEEL_DOWN if dir > 0 else MOUSE_BUTTON_WHEEL_UP
var w := InputEventMouseButton.new()
w.position = pos
w.global_position = pos
w.button_index = idx
w.pressed = true
main.get_viewport().push_input(w, true)
var w2 := InputEventMouseButton.new()
w2.position = pos
w2.global_position = pos
w2.button_index = idx
w2.pressed = false
main.get_viewport().push_input(w2, true)
# 窗板定位(窗壳视图子树固定链:遮罩 ColorRect → CenterContainer → 窗板 PanelContainer——
# screen_flow_probe._window_panel 同式,探针族共用单点)
static func window_panel(view: Control) -> PanelContainer:
if view == null or view.get_child_count() == 0:
return null
var dim := view.get_child(0)
if dim.get_child_count() == 0:
return null
var center := dim.get_child(0)
if center.get_child_count() == 0 or not (center.get_child(0) is PanelContainer):
return null
return center.get_child(0) as PanelContainer
# 折叠区行滚入窗板可视区(真输入滚轮驱动;滚动不重建行,rect 随滚更新)。
# 判据:钮 rect 整体落在窗板内(上缘留 8px、下缘让开底栏 64px——返回营地钮带高约 56px)。
# 保险丝 60 次滚叶防异常内容死循环;窗板缺失或钮句柄失效即返回(调用方前置断言兜底)。
static func scroll_button_into_view(main, view: Control, btn: Control) -> void:
var panel := window_panel(view)
if panel == null or btn == null:
return
var guard := 60
while guard > 0:
var r := btn.get_global_rect()
var pr := panel.get_global_rect()
if r.position.y >= pr.position.y + 8.0 and r.end.y <= pr.end.y - 64.0:
return
var down := r.end.y > pr.end.y - 64.0
push_wheel(main, pr.get_center(), 1 if down else -1)
guard -= 1
await main.get_tree().process_frame
# 真输入拖拽沿(press→motion×steps→release 成对经 push_input 进引擎输入管线——卷轴视口批
# R3/R5/R6 驱动沿;沿 push_click_at 同式。relative 按步显式置值——合成事件的位移量由事件
# 自身承载,被测面按 relative 逐帧平移)
@@ -167,7 +239,7 @@ static func push_drag_at(main, pos: Vector2, delta: Vector2, steps: int = 4) ->
# 真输入滚轮沿(单档滚轮事件经 push_input——卷轴视口批 R4 驱动沿;方向映射在被测面,
# 本工具只发原始滚轮档)
# 本工具只发原始滚轮档)。press+release 成对(同 push_wheel 注——只发 press 钉死鼠标焦点)。
static func push_wheel_at(main, pos: Vector2, wheel_down: bool = true) -> void:
var ev := InputEventMouseButton.new()
ev.position = pos
@@ -175,6 +247,12 @@ static func push_wheel_at(main, pos: Vector2, wheel_down: bool = true) -> void:
ev.button_index = MOUSE_BUTTON_WHEEL_DOWN if wheel_down else MOUSE_BUTTON_WHEEL_UP
ev.pressed = true
main.get_viewport().push_input(ev, true)
var ev2 := InputEventMouseButton.new()
ev2.position = pos
ev2.global_position = pos
ev2.button_index = MOUSE_BUTTON_WHEEL_DOWN if wheel_down else MOUSE_BUTTON_WHEEL_UP
ev2.pressed = false
main.get_viewport().push_input(ev2, true)
# 营地经进档开层(卡② mn9 新路径——campbtn 已撤;槽模板注入隔离命名空间,防碰真实槽
+28 -35
View File
@@ -8,9 +8,11 @@
extends Node
const ProbeUiUtil := preload("res://probe/probe_ui_util.gd")
const ProbeCoverageRecorder := preload("res://probe/probe_coverage_recorder.gd")
var main: Node
var _problems: Array[String] = []
var _recorder: RefCounted = null # 真输入覆盖登记件(probe_coverage_recorder.gd——attach 后全探针真输入自动记账)
func _ready() -> void:
@@ -20,6 +22,10 @@ func _ready() -> void:
await get_tree().process_frame
await get_tree().process_frame
# 真输入覆盖登记(命中控件自动记账,收尾落 tests/.gates/coverage/——覆盖面生长不靠人记)
_recorder = ProbeCoverageRecorder.new()
_recorder.attach(main, "screen_flow")
# 隔离存档(全 worktree 共享同一份 user://save.json——探针必须换注,禁碰真实档)
ProbeUiUtil.isolate_save(main, "smoke_screen_flow", false, false, false)
await get_tree().process_frame
@@ -35,6 +41,7 @@ func _ready() -> void:
await _step_abandon_to_end()
await _step_end_back_to_menu()
_recorder.dump_js()
if _problems.is_empty():
print("[screen_flow_probe] PASS — 六步主流程+回退路径全绿(屏级互斥:任意时刻 ≤1 屏可见)")
get_tree().quit(0)
@@ -122,11 +129,11 @@ func _step_camp_back_to_menu() -> void:
if main.ui.modals.camp.scene == null:
_problems.append("③营地→菜单:营地场景根句柄缺失〔句柄子容器批 V1〕")
return
main.ui.modals.camp.scene.object_pressed.emit(&"campfire")
ProbeUiUtil.push_click_at(main, main.ui.modals.camp.scene.get_hotspot_rect(&"campfire").get_center()) # 真输入(营地场景 _gui_input 命中——大门段同式)
await _frames()
if not main.ui.modals.menu.confirm_dialog.visible:
_problems.append("③篝火:二次确认弹窗未现(camp.md v2.2 必须先确认)〔营地图批 V7〕")
main.ui.modals.menu.confirm_dialog.confirmed.emit() # 二次确认离场(confirmed 先于关层——卡④ T4)
ProbeUiUtil.push_click(main, main.ui.modals.menu.confirm_dialog._confirm_btn) # 真点「确认」离场(confirmed 先于关层——卡④ T4)
await _frames()
_expect_exclusive("③营地→菜单(回退)", "菜单")
@@ -150,7 +157,7 @@ func _step_save_select_open_back() -> void:
if back_btn == null:
_problems.append("[savesel] sl10 返回钮缺失(页壳顶条)")
else:
back_btn.pressed.emit()
ProbeUiUtil.push_click(main, back_btn) # 真点「返回」→ 关选档层
await get_tree().process_frame
if main.ui.modals.menu.save_select_overlay.visible or not main.ui.screens.start_overlay.visible:
_problems.append("[savesel] sl10 返回后选档未隐藏或开始层未恢复(关闭链失守)")
@@ -228,7 +235,7 @@ func _step_end_back_to_menu() -> void:
if main.ui.modals.run.end_overlay == null or not main.ui.modals.run.end_overlay.visible:
_problems.append("⑦终局→菜单:前置不在终局")
return
main.ui.modals.run.end_overlay.continue_pressed.emit()
ProbeUiUtil.push_click(main, main.ui.modals.run.end_overlay._btn) # 真点「继续」(_btn=continue_pressed 同源句柄)
await _frames(3)
_expect_exclusive("⑦终局→菜单", "菜单")
if main.run_scope != null and main.run_scope.is_adventure:
@@ -254,8 +261,10 @@ func _step_camp_esc_and_hud() -> void:
await _frames()
if main.ui.modals.camp.esc_overlay.visible:
_problems.append("[esc] echo 事件唤起浮窗(触发沿 pressed 且非 echo 失守)〔ESC批 V1〕")
main.ui.modals.camp.scene.object_pressed.emit(&"dummy") # 子界面开态
ProbeUiUtil.push_click_at(main, main.ui.modals.camp.scene.get_hotspot_rect(&"dummy").get_center()) # 子界面开态(真输入命中)
await _frames()
if not main.ui.modals.camp.tree.visible:
_problems.append("[esc] 子界面未随场景点选打开(后续关窗断言的前置落点)")
_push_esc()
await _frames()
if main.ui.modals.camp.tree.visible:
@@ -285,7 +294,7 @@ func _step_camp_esc_and_hud() -> void:
if not main.ui.modals.camp.esc_overlay.visible or main.ui.screens.start_overlay.visible:
_problems.append("[esc] 关设置后未回浮窗或回弹主菜单(契约 3 语境分支失守)〔ESC批 V4〕〔ESC批 B3〕")
# 返回主菜单动线(契约 4——浮窗自收+营地隐+选档收+菜单显)
main.ui.modals.camp.esc_overlay.return_requested.emit()
ProbeUiUtil.push_click(main, main.ui.modals.camp.esc_overlay._return_btn) # 真点「返回主菜单」
await _frames()
if main.ui.modals.camp.esc_overlay.visible or main.ui.modals.camp.scene.visible or main.ui.modals.menu.save_select_overlay.visible or not main.ui.screens.start_overlay.visible:
_problems.append("[esc] 返回主菜单动线失守(浮窗自收/营地隐/选档收/菜单显)〔ESC批 V3〕〔ESC批 B3〕")
@@ -323,11 +332,11 @@ func _step_camp_window_shell() -> void:
if prev_back == null:
_problems.append("[winshell] %s 底栏「返回营地」钮缺失〔窗口化批 V4〕" % String(opened.back()))
else:
prev_back.pressed.emit()
ProbeUiUtil.push_click(main, prev_back) # 真点「返回营地」关前窗
await _frames()
if prev.visible or not main.ui.modals.camp.scene.visible:
_problems.append("[winshell] %s 关闭钮按下未隐窗或场景根失守〔窗口化批 V4〕" % String(opened.back()))
main.ui.modals.camp.scene.object_pressed.emit(k)
ProbeUiUtil.push_click_at(main, main.ui.modals.camp.scene.get_hotspot_rect(k).get_center()) # 真输入开窗(场景 _gui_input 命中)
await _frames()
opened.append(k)
var view: Control = views[k]
@@ -362,7 +371,10 @@ func _step_camp_window_shell() -> void:
if vis_n != 1:
_problems.append("[winshell] %s 开态五窗 visible 计数=%d 应=1(互斥单开失守)〔窗口化批 V5〕" % [String(k), vis_n])
# 树窗点选遮挡(V5b——树窗开态向锻炉命中区推真实鼠标点:被遮罩挡=零新窗零响应)
main.ui.modals.camp.scene.object_pressed.emit(&"dummy")
# 真输入化订正:前窗(图鉴)先经真实关闭钮清账再开树窗——emit 直发可穿透遮罩开窗,
# 真输入下「无窗态场景命中」是唯一合法开窗路径(遮罩语义由本段正面验证)。
await _close_all_windows(views)
ProbeUiUtil.push_click_at(main, main.ui.modals.camp.scene.get_hotspot_rect(&"dummy").get_center())
await _frames()
var tree_view: Control = views[&"dummy"]
if not tree_view.visible:
@@ -370,8 +382,7 @@ func _step_camp_window_shell() -> void:
else:
var hotspot: Rect2 = main.ui.modals.camp.scene.get_hotspot_rect(&"forge")
var c := hotspot.get_center()
_push_click(c)
_push_release(c)
ProbeUiUtil.push_click_at(main, c) # 遮挡试探真点击——遮罩生效则零命中零响应
await _frames()
var vis_n2 := 0
for w in five:
@@ -383,7 +394,7 @@ func _step_camp_window_shell() -> void:
# 随栈 close 恢复语义逐层清账,V6/V8 从干净单窗态起步)
await _close_all_windows(views)
# V6 ESC 先关窗(生涯窗开态 Esc→关生涯窗、浮窗不唤起)
main.ui.modals.camp.scene.object_pressed.emit(&"notebook")
ProbeUiUtil.push_click_at(main, main.ui.modals.camp.scene.get_hotspot_rect(&"notebook").get_center())
await _frames()
if not career.visible:
_problems.append("[winshell] 生涯窗未开(V6 前置中断)〔窗口化批 V6〕")
@@ -395,7 +406,7 @@ func _step_camp_window_shell() -> void:
if main.ui.modals.camp.esc_overlay.visible:
_problems.append("[winshell] 关窗同拍唤起浮窗(应无窗才进浮窗)〔窗口化批 V6〕")
# V7 内容超高窗内滚动(树窗+真实投影喂加长行册)
main.ui.modals.camp.scene.object_pressed.emit(&"dummy")
ProbeUiUtil.push_click_at(main, main.ui.modals.camp.scene.get_hotspot_rect(&"dummy").get_center())
await _frames()
if not tree_view.visible:
_problems.append("[winshell] 树窗未开(V7 前置中断)〔窗口化批 V7〕")
@@ -420,7 +431,7 @@ func _step_camp_window_shell() -> void:
# 清窗经真实钮(同 V6 前置——干净单窗态起步)
await _close_all_windows(views)
# V8 生涯同壳(窗板同几何+返回钮在位+内容列滚动面在)
main.ui.modals.camp.scene.object_pressed.emit(&"notebook")
ProbeUiUtil.push_click_at(main, main.ui.modals.camp.scene.get_hotspot_rect(&"notebook").get_center())
await _frames()
if not career.visible:
_problems.append("[winshell] 生涯窗未开(V8 前置中断)〔窗口化批 V8〕")
@@ -445,7 +456,7 @@ func _step_camp_window_shell() -> void:
# 段尾收闭:生涯窗经真实「返回」钮关闭(on_camp_closed 关闭名单不含生涯窗——防残留泄段)
var career_back: Button = ProbeUiUtil.find_button(career, "返回", "prefix")
if career_back != null:
career_back.pressed.emit()
ProbeUiUtil.push_click(main, career_back) # 真点「返回」收闭生涯窗
await _frames()
if career.visible:
_problems.append("[winshell] 生涯「返回」钮未隐窗〔窗口化批 V4〕")
@@ -463,7 +474,7 @@ func _close_all_windows(views: Dictionary) -> void:
if back == null:
_problems.append("[winshell] %s 底栏「返回营地」钮缺失〔窗口化批 V4〕" % String(k))
continue
back.pressed.emit()
ProbeUiUtil.push_click(main, back) # 真点「返回营地」逐窗清账
await _frames()
@@ -480,25 +491,7 @@ func _window_panel(view: Control) -> PanelContainer:
return center.get_child(0) as PanelContainer
# 真实鼠标合成(press+release 成对——BaseButton pressed 信号默认 release 触发,ui_input_probe :718 先例)
func _push_click(pos: Vector2) -> void:
var ev := InputEventMouseButton.new()
ev.position = pos
ev.global_position = pos
ev.button_index = MOUSE_BUTTON_LEFT
ev.pressed = true
main.get_viewport().push_input(ev, true)
func _push_release(pos: Vector2) -> void:
var ev := InputEventMouseButton.new()
ev.position = pos
ev.global_position = pos
ev.button_index = MOUSE_BUTTON_LEFT
ev.pressed = false
main.get_viewport().push_input(ev, true)
# 键盘真输入合成(鼠标点击统一走 ProbeUiUtil.push_click_at——覆盖登记单点通道)
func _push_esc(echo := false) -> void:
var ev := InputEventKey.new()
ev.keycode = KEY_ESCAPE
+76 -42
View File
@@ -11,8 +11,10 @@ extends Node
const AutoPlayer := preload("res://sim/auto_player.gd") # O4:自动玩家策略单一实现
const ProbeUiUtil := preload("res://probe/probe_ui_util.gd") # T6 收口:树寻址/隔离样板单点
const ProbeCoverageRecorder := preload("res://probe/probe_coverage_recorder.gd")
var main: Node
var _recorder: RefCounted = null # 真输入覆盖登记件(probe_coverage_recorder.gd——attach 后全探针真输入自动记账)
var _last_state := {}
var _stall_frames := 0
var _closed_via_abandon := false # T17——预算耗尽经放弃路径收口(断点段返回断言豁免位)
@@ -48,6 +50,9 @@ func _ready() -> void:
main = load("res://presentation/main.tscn").instantiate()
add_child(main)
await get_tree().process_frame
# 真输入覆盖登记(命中控件自动记账,收尾落 tests/.gates/coverage/——覆盖面生长不靠人记)
_recorder = ProbeCoverageRecorder.new()
_recorder.attach(main, "ui_click")
var t_ui_ready := Time.get_ticks_msec() - t0
# 调试浮窗 boot 不开(菜单优先——入口=UI 最顶层全局浮层角标,
# 任何 UI 不遮挡;浮层角标遮挡判据见设置面段)
@@ -67,7 +72,7 @@ func _ready() -> void:
await get_tree().process_frame
if not main.ui.modals.camp.scene.visible:
_problems.append("营地场景根未打开(进档开层新路径)〔句柄子容器批 V1〕")
main.ui.modals.camp.scene.object_pressed.emit(&"dummy") # 场景点选真实路由(木桩→天赋树子界面——卡④ T5)
ProbeUiUtil.push_click_at(main, main.ui.modals.camp.scene.get_hotspot_rect(&"dummy").get_center()) # 场景点选真输入(木桩→天赋树子界面——卡④ T5;_gui_input 命中)
await get_tree().process_frame
if main.ui.modals.camp.tree._tree_buy_btns.is_empty():
_problems.append("树淬炼购买钮缺失(四柜台拆解后树=独立子界面)〔句柄子容器批 V1〕")
@@ -85,7 +90,8 @@ func _ready() -> void:
else:
var bal_before: int = iso_c.meta_state.keystone_balance()
var price: int = int(main.db.crossrun.meta_tree.row_by_id("sp_hp").get("price", 0))
buy_btn.pressed.emit() # 合成购买点击 → _on_camp_tree_buy → meta.tree_buy(写调用唯一归组合根)
await ProbeUiUtil.scroll_button_into_view(main, main.ui.modals.camp.tree, buy_btn) # 折叠区行先滚入窗板可视区(真输入滚轮)
ProbeUiUtil.push_click(main, buy_btn) # 真点购买 → _on_camp_tree_buy → meta.tree_buy(写调用唯一归组合根)
await get_tree().process_frame
if int(iso_c.meta_state.keystone_balance()) != bal_before - price:
_problems.append("营地树购买未出账(%d→%d 应 −%d)〔句柄子容器批 V1〕〔营地图批 V4〕" % [bal_before, iso_c.meta_state.keystone_balance(), price])
@@ -99,7 +105,8 @@ func _ready() -> void:
if String(card.node_id) == "att_socket1":
socket_btn = card.btn
if socket_btn != null:
socket_btn.pressed.emit()
await ProbeUiUtil.scroll_button_into_view(main, main.ui.modals.camp.tree, socket_btn)
ProbeUiUtil.push_click(main, socket_btn) # 真点解锁(点亮同调位前置)
await get_tree().process_frame
main.ui.modals.camp.tree.refresh_camp(main._camp_data())
await get_tree().process_frame
@@ -110,7 +117,8 @@ func _ready() -> void:
if fork_btn == null:
_problems.append("Fork 解锁钮缺失(点击路径)〔句柄子容器批 V1〕")
else:
fork_btn.pressed.emit()
await ProbeUiUtil.scroll_button_into_view(main, main.ui.modals.camp.tree, fork_btn) # fork 行在折叠区(rect 越窗底栏——实证误中「返回营地」钮)
ProbeUiUtil.push_click(main, fork_btn) # 真点解锁(展开 a/b 双态钮)
await get_tree().process_frame
main.ui.modals.camp.tree.refresh_camp(main._camp_data())
await get_tree().process_frame
@@ -121,7 +129,8 @@ func _ready() -> void:
if side_b == null:
_problems.append("Fork 双态切换钮缺失(解锁后应展开 a/b 钮)〔句柄子容器批 V1〕")
else:
side_b.pressed.emit() # 合成切态点击 → _on_camp_fork_toggle → meta.tree_toggle_fork
await ProbeUiUtil.scroll_button_into_view(main, main.ui.modals.camp.tree, side_b)
ProbeUiUtil.push_click(main, side_b) # 真点切态 → _on_camp_fork_toggle → meta.tree_toggle_fork
await get_tree().process_frame
var iso_fk = load("res://core/save_manager.gd").new("user://smoke_click_probe_camp_slot_1.json")
iso_fk.load_from_disk()
@@ -130,9 +139,15 @@ func _ready() -> void:
# ---- 营地灰字取色断言(迭代36 wc4——四子界面拆分后按视图分扫;卡④ T5 迁移):
# 图鉴锁文→codex 视图(锚随形态订正去『柜台』);秘宝行未解锁/同调形态/档位/刷新行→treasure 视图
# _workshop_holder 子树。font_color 逐处 == STONE_DEEP;树链段已闭树界面。
main.ui.modals.camp.tree.closed.emit() # 树界面返回(关层归 presenter 单口)
var tree_back: Button = ProbeUiUtil.find_button(main.ui.modals.camp.tree, "返回营地")
if tree_back == null:
_problems.append("树界面「返回营地」钮缺失(真输入化驱动中断)〔句柄子容器批 V1〕")
else:
ProbeUiUtil.push_click(main, tree_back) # 真点返回(关层归 presenter 单口——closed 同源钮)
await get_tree().process_frame
main.ui.modals.camp.scene.object_pressed.emit(&"chest") # 宝箱→秘宝匣(灰字四源位宿主)
if main.ui.modals.camp.tree.visible:
_problems.append("树窗「返回营地」真点击后未隐窗(真输入化补钉——落点判定)〔句柄子容器批 V1〕")
ProbeUiUtil.push_click_at(main, main.ui.modals.camp.scene.get_hotspot_rect(&"chest").get_center()) # 宝箱→秘宝匣(灰字四源位宿主)
await get_tree().process_frame
var ws_labels: Array = []
_collect_labels_by_text(main.ui.modals.camp.treasure._workshop_holder, ws_labels)
@@ -169,9 +184,13 @@ func _ready() -> void:
_problems.append("营地灰字:刷新行未用 STONE_DEEP(wc4 :525)〔句柄子容器批 V1〕")
if not refresh_inked:
_problems.append("营地灰字:刷新行 label 未找到(wc4 定位锚失守)〔句柄子容器批 V1〕")
main.ui.modals.camp.treasure.closed.emit()
var treasure_back: Button = ProbeUiUtil.find_button(main.ui.modals.camp.treasure, "返回营地")
if treasure_back == null:
_problems.append("秘宝匣「返回营地」钮缺失(真输入化驱动中断)〔句柄子容器批 V1〕")
else:
ProbeUiUtil.push_click(main, treasure_back) # 真点返回(closed 同源钮)
await get_tree().process_frame
main.ui.modals.camp.scene.object_pressed.emit(&"board") # 布告板→图鉴占位(锁文宿主)
ProbeUiUtil.push_click_at(main, main.ui.modals.camp.scene.get_hotspot_rect(&"board").get_center()) # 布告板→图鉴占位(锁文宿主)
await get_tree().process_frame
var codex_labels: Array = []
_collect_labels_by_text(main.ui.modals.camp.codex, codex_labels)
@@ -181,7 +200,11 @@ func _ready() -> void:
for l in atlas_locks:
if l.get_theme_color("font_color") != UiKit.STONE_DEEP:
_problems.append("营地灰字:图鉴锁文未用 STONE_DEEP(wc4 :167)〔句柄子容器批 V1〕〔营地图批 V6〕")
main.ui.modals.camp.codex.closed.emit()
var codex_back: Button = ProbeUiUtil.find_button(main.ui.modals.camp.codex, "返回营地")
if codex_back == null:
_problems.append("图鉴「返回营地」钮缺失(真输入化驱动中断)〔句柄子容器批 V1〕")
else:
ProbeUiUtil.push_click(main, codex_back) # 真点返回(closed 同源钮)
await get_tree().process_frame
main._on_camp_closed() # 关层收闭(营地与选档层同收+开始层恢复——卡④ T3)
await get_tree().process_frame
@@ -213,7 +236,7 @@ func _ready() -> void:
_problems.append("[savesel] 槽1 新档钮缺失(sl5/sl12 驱动中断)")
else:
var samples: Array = []
new_btn.pressed.emit() # 真实按钮信号路径 → _on_save_select_new → create_slot+open_camp
ProbeUiUtil.push_click(main, new_btn) # 真点「新档」→ _on_save_select_new → create_slot+open_camp
for fi in 3:
await get_tree().process_frame
samples.append([main.ui.modals.menu.save_select_overlay.visible, main.ui.modals.camp.scene.visible, main.ui.screens.start_overlay.visible])
@@ -239,7 +262,7 @@ func _ready() -> void:
if load_btn == null:
_problems.append("[savesel] 槽2 进档钮缺失(sl6 驱动中断——夹具应有档)")
else:
load_btn.pressed.emit()
ProbeUiUtil.push_click(main, load_btn) # 真点「进档」→ 激活槽2 开营地
await get_tree().process_frame
if main.save_manager.slots.active_slot() != 2:
_problems.append("[savesel] sl6 进档链失守(active=%d 应=2——激活该槽)" % main.save_manager.slots.active_slot())
@@ -254,7 +277,7 @@ func _ready() -> void:
if del_btn == null:
_problems.append("[savesel] 槽2 删档钮缺失(sl7 驱动中断)")
else:
del_btn.pressed.emit()
ProbeUiUtil.push_click(main, del_btn) # 真点「删档」一击只开窗
await get_tree().process_frame
if not main.ui.modals.menu.confirm_dialog.is_open():
_problems.append("[savesel] sl7 一击未只开确认窗(直删形态)")
@@ -265,7 +288,7 @@ func _ready() -> void:
_problems.append("[savesel] sl7 确认窗未经模态栈开(直开旁路——wr2 单口失守)")
elif not main.ui.flow.locks_input():
_problems.append("[savesel] sl7 确认窗开时输入锁未生效(锁域失守——wr2)")
main.ui.modals.menu.confirm_dialog.cancelled.emit() # 取消路径(真实信号——cancelled 钮同源)
ProbeUiUtil.push_click(main, main.ui.modals.menu.confirm_dialog._cancel_btn) # 真点「取消」(cancelled 钮同源)
await get_tree().process_frame
if main.ui.modals.menu.confirm_dialog.is_open() or not FileAccess.file_exists(csel_tpl % 2):
_problems.append("[savesel] sl7 取消路径失守(应只关窗、文件仍在)")
@@ -273,9 +296,9 @@ func _ready() -> void:
_problems.append("[savesel] sl8 取消后消费方状态量未清(分派面残留)")
if main.ui.flow.stack._occluded.has(main.ui.modals.menu.confirm_dialog):
_problems.append("[savesel] sl7 取消后确认窗遮挡账未清(关侧未走模态栈——wr2)")
del_btn.pressed.emit() # 再开窗走确认路径
ProbeUiUtil.push_click(main, del_btn) # 真点再开窗走确认路径
await get_tree().process_frame
main.ui.modals.menu.confirm_dialog.confirmed.emit() # 确认路径(真实信号——confirmed 钮同源)
ProbeUiUtil.push_click(main, main.ui.modals.menu.confirm_dialog._confirm_btn) # 真点「确认」(confirmed 钮同源)
await get_tree().process_frame
if FileAccess.file_exists(csel_tpl % 2) or main.ui.modals.menu.confirm_dialog.is_open():
_problems.append("[savesel] sl7 确认路径失守(应删档+关窗)")
@@ -343,7 +366,7 @@ func _ready() -> void:
if settings_close == null:
_problems.append("设置面返回按钮缺失")
else:
settings_close.pressed.emit()
ProbeUiUtil.push_click(main, settings_close) # 真点「返回」收设置面
await get_tree().process_frame
if main.ui.modals.menu.settings_overlay.visible or not main.ui.screens.start_overlay.visible:
_problems.append("设置面关闭后未恢复开始层")
@@ -377,7 +400,7 @@ func _ready() -> void:
if career_close == null:
_problems.append("生涯页返回按钮缺失")
else:
career_close.pressed.emit()
ProbeUiUtil.push_click(main, career_close) # 真点「返回」收生涯页
await get_tree().process_frame
if main.ui.modals.menu.career_overlay.visible or not main.ui.screens.start_overlay.visible:
_problems.append("生涯页关闭后未恢复开始层")
@@ -463,7 +486,11 @@ func _ready() -> void:
# 分派链序:商店层=选路页上的模态覆盖(停点呈现为
# level_screen+shop 双 visible)——必须先于 level_screen 分派,否则下层候选点击
# 空转烧尽预算(实证:lv2 商店停点双 visible 循环 12000 帧 done=false)。
main.ui.modals.menu.shop_overlay.leave_pressed.emit()
var shop_leave: Button = ProbeUiUtil.find_button(main.ui.modals.menu.shop_overlay, "离开商店")
if shop_leave == null:
_problems.append("商店「离开商店」钮缺失(真输入化驱动中断)")
else:
ProbeUiUtil.push_click(main, shop_leave) # 真点离店(leave_pressed 同源钮)放行选路
_seals_placed.clear()
continue
if main.ui.modals.run.level_screen.visible:
@@ -476,12 +503,13 @@ func _ready() -> void:
if mpb == null or not mpb.visible or mpb.disabled:
_problems.append("地图屏状态角标缺失/未点亮/被置灰(P1 丁)")
else:
mpb.pressed.emit()
ProbeUiUtil.push_click(main, mpb) # 真点地图屏角标开状态面板
if not main.ui.modals.run.status_panel.visible or not main.ui.modals.run.level_screen.visible:
_problems.append("地图屏角标开状态面板失败或地图屏被误藏(P2 甲层级失守)")
await get_tree().process_frame # 面板当帧开——容器排版帧后钮 rect 才有效(真点击依赖排版)
var rbtn2: Button = ProbeUiUtil.find_button(main.ui.modals.run.status_panel, "继续", "prefix")
if rbtn2 != null:
rbtn2.pressed.emit()
ProbeUiUtil.push_click(main, rbtn2) # 真点「继续」关面板
if main.ui.modals.run.status_panel.visible or not main.ui.modals.run.level_screen.visible:
_problems.append("状态面板关闭后地图屏态失守(恢复语义破)")
# 地图屏加成帽换代断言(迭代36 卡A wa2——「特性」→「属性值提升」;帽 Label 常在树;
@@ -519,7 +547,8 @@ func _ready() -> void:
if next_btn == null:
_problems.append("大奖励屏确认按钮缺失")
else:
next_btn.pressed.emit()
await get_tree().process_frame # 停点层当帧开——排版帧后真点击
ProbeUiUtil.push_click(main, next_btn) # 真点「进入下一层」推进 L2(落点判定见下)
await get_tree().process_frame
await get_tree().process_frame
if main.run_scope.runner == null or int(main.run_scope.runner.level_row.get("level_index", -1)) != 2:
@@ -540,7 +569,7 @@ func _ready() -> void:
if main.ui.modals.run.end_overlay.verdict_text() == "":
_problems.append("胜利结算框判定行缺失")
_settlement_confirms += 1
main.ui.modals.run.end_overlay._btn.pressed.emit() # 「下一节点」真实信号路径
ProbeUiUtil.push_click(main, main.ui.modals.run.end_overlay._btn) # 真点「下一节点」(结算框已过帧排版)
await get_tree().process_frame
await get_tree().process_frame
if main.run_scope.settlement_stop_timer.is_armed() or not main.ui.modals.run.level_screen.visible:
@@ -555,7 +584,7 @@ func _ready() -> void:
# (_adventure_mode)在真终局仍 true——会把真终局当中途面点掉返回键(实证)。
var _adv_running: bool = main.run_scope.adventure != null and String(main.run_scope.adventure.state) == "running"
if main.run_scope.is_adventure and _adv_running:
main.ui.modals.run.end_overlay._btn.pressed.emit()
ProbeUiUtil.push_click(main, main.ui.modals.run.end_overlay._btn) # 中途结算面真点「继续」推进
continue
if not main.ui.flow.locks_input():
_problems.append("终局 overlay 打开时输入未锁定")
@@ -612,7 +641,7 @@ func _ready() -> void:
if main.ui.modals.run.end_overlay.is_open():
# 中途结算面(state=running)=点继续推进;真终局(settling)=断点段前提满足,收口。
if main.run_scope.is_adventure and main.run_scope.adventure != null and String(main.run_scope.adventure.state) == "running":
main.ui.modals.run.end_overlay._btn.pressed.emit()
ProbeUiUtil.push_click(main, main.ui.modals.run.end_overlay._btn) # 中途结算面真点「继续」推进
else:
break
elif main.ui.modals.run.curio_overlay.visible:
@@ -626,18 +655,20 @@ func _ready() -> void:
elif main.ui.modals.run.node_overlay.visible:
main.ui.modals.run.node_overlay.press_continue()
elif main.ui.modals.menu.shop_overlay.visible:
main.ui.modals.menu.shop_overlay.leave_pressed.emit()
var shop_leave2: Button = ProbeUiUtil.find_button(main.ui.modals.menu.shop_overlay, "离开商店")
if shop_leave2 != null:
ProbeUiUtil.push_click(main, shop_leave2) # 真点离店(收口段同主循环分派)
elif main.ui.screens.inter_overlay.visible:
var next_btn2: Button = ProbeUiUtil.find_button(main.ui.screens.inter_overlay, "进入下一层", "prefix")
if next_btn2 != null:
next_btn2.pressed.emit()
ProbeUiUtil.push_click(main, next_btn2) # 真点「进入下一层」(收口段)
elif main.ui.modals.run.level_screen.visible:
var abandon_btn: Button = ProbeUiUtil.find_button(main.ui.modals.run.level_screen, "放弃", "prefix")
if abandon_btn != null:
_closed_via_abandon = true
abandon_btn.pressed.emit()
ProbeUiUtil.push_click(main, abandon_btn) # 真点「放弃」一击
await get_tree().process_frame
abandon_btn.pressed.emit() # 二击确认(3s 窗内)
ProbeUiUtil.push_click(main, abandon_btn) # 真点二击确认(3s 窗内)
for i in 10:
await get_tree().process_frame
break
@@ -665,7 +696,7 @@ func _ready() -> void:
if main.ui.modals.run.end_overlay._btn == null:
_problems.append("断点段:终局面返回按钮缺失")
else:
main.ui.modals.run.end_overlay._btn.pressed.emit() # 真实按钮信号路径(_btn=继续/返回共用句柄)
ProbeUiUtil.push_click(main, main.ui.modals.run.end_overlay._btn) # 真点「返回」(_btn=继续/返回共用句柄)
await get_tree().process_frame
if not main.ui.screens.start_overlay.visible:
# 豁免:放弃路径收口时终局「返回」后仍挂三选一/关卡推进残留态(停点链),
@@ -693,7 +724,7 @@ func _ready() -> void:
_problems.append("大门无断点:未确认即进冒险〔营地图批 V3〕")
if main.ui.modals.menu.confirm_dialog._secondary_btn.visible:
_problems.append("大门无断点:次钮应隐藏(两出口形态)〔营地图批 V3〕")
main.ui.modals.menu.confirm_dialog.cancelled.emit()
ProbeUiUtil.push_click(main, main.ui.modals.menu.confirm_dialog._cancel_btn) # 真点「取消」(大门取消出口)
await get_tree().process_frame
if not main.ui.modals.camp.scene.visible or main.run_scope.is_adventure or main._camp_data()["balance"] != bal_gate:
_problems.append("大门取消:未无变更退回营地(写口零调用/营地根 visible 失守)〔营地图批 V3〕")
@@ -795,6 +826,7 @@ func _ready() -> void:
exit_code = 1
if not _problems.is_empty():
exit_code = 1
_recorder.dump_js() # 收尾落盘(大门段/开场行段点击在循环退出后——落盘必须在 quit 前)
get_tree().quit(exit_code)
@@ -821,7 +853,7 @@ func _run_modal_probes_once() -> void:
# ① 图例模态(pattern 6):打开 → 遮挡物隐藏 + 输入锁定;关闭 → 恢复
if not _tested_legend:
_tested_legend = true
main.ui.battle.board_zone.ap_bar._btn_legend.pressed.emit()
ProbeUiUtil.push_click(main, main.ui.battle.board_zone.ap_bar._btn_legend) # 真点图例钮开层
if not main.ui.modals.run.legend_overlay.is_open():
_problems.append("图例打开失败")
if main.ui.modals.run.end_overlay.is_open() or main.ui.screens.start_overlay.visible:
@@ -851,7 +883,7 @@ func _run_modal_probes_once() -> void:
_problems.append("战斗屏状态角标缺失/未点亮/被置灰(P1 丁入口失守 visible=%s disabled=%s)"
% [str(pbtn != null and pbtn.visible), str(pbtn != null and pbtn.disabled)])
else:
pbtn.pressed.emit() # 真实按钮信号路径 → 组合根 _on_status_badge_pressed
ProbeUiUtil.push_click(main, pbtn) # 真点状态角标 → 组合根 _on_status_badge_pressed
if not main.ui.modals.run.status_panel.visible:
_problems.append("状态面板未随角标打开(组合根接线失守)")
if not main.ui.flow.locks_input():
@@ -903,30 +935,32 @@ func _run_modal_probes_once() -> void:
if resume_btn == null:
_problems.append("状态面板关闭钮缺失(「继续」)")
else:
resume_btn.pressed.emit()
await get_tree().process_frame # 面板当帧开——排版帧后真点击
ProbeUiUtil.push_click(main, resume_btn) # 真点「继续」关面板恢复
if main.ui.modals.run.status_panel.visible or main.ui.flow.locks_input():
_problems.append("状态面板关闭后未恢复(visible=%s locked=%s——R3 残留)"
% [str(main.ui.modals.run.status_panel.visible), str(main.ui.flow.locks_input())])
# P1 守卫:图例开着 → 两处入口角标置灰;置灰态点击被处理器兜住(探针 emit 绕过 disabled)
main.ui.battle.board_zone.ap_bar._btn_legend.pressed.emit()
# P1 守卫:图例开着 → 两处入口角标置灰;置灰态点击被处理器兜住
#(置灰态穿透判定保留 emit 直发——真输入到不了置灰钮,处理器级防线只能以〔接线断言〕钉住)
ProbeUiUtil.push_click(main, main.ui.battle.board_zone.ap_bar._btn_legend) # 真点图例钮复开
if not main.ui.battle.board_zone.status_badge.disabled:
_problems.append("图例开着时战斗屏状态角标未置灰(P1 守卫失守)")
main.ui.battle.board_zone.status_badge.pressed.emit()
main.ui.battle.board_zone.status_badge.pressed.emit() # 〔接线断言〕置灰态 emit 穿透——钉处理器兜底(真输入被 disabled 拦,测不到此层)
if main.ui.modals.run.status_panel.visible:
_problems.append("置灰态仍可开状态面板(守卫兜底失守)")
main.ui.modals.run.legend_overlay.close()
# ③ tooltip(状态效果槽悬停——来源+语义+剩余):有槽可见时触发
# ③ tooltip(状态效果槽悬停——来源+语义+剩余):有槽可见时触发(真输入 motion 进管线触发 entered/exited 沿)
if not _tested_tooltip:
var row = main.ui.battle.stage.hero_standee.slot_row # 状态 tooltip 载体(B 区站桩)
for child in row.get_children():
if child is PanelContainer and child.visible and child.has_meta("eff"):
_tested_tooltip = true
child.mouse_entered.emit()
ProbeUiUtil.push_motion(main, (child as PanelContainer).get_global_rect().get_center())
if not row._tip.visible:
_problems.append("状态槽 tooltip 未显示")
elif not row._tip_label.text.contains("来源") or not row._tip_label.text.contains("剩余"):
_problems.append("状态槽 tooltip 缺来源/剩余(%s)" % row._tip_label.text.replace("\n", "|"))
child.mouse_exited.emit()
ProbeUiUtil.push_motion(main, Vector2(-40, -40)) # 移出一切控件(视口外沿触发 exited)
if row._tip.visible:
_problems.append("状态槽 tooltip 未随移出隐藏")
break
@@ -1027,8 +1061,8 @@ func _random_unknown_adjacent(minesweeper) -> Vector2i:
func _click_stop() -> void:
# 停手走真实按钮信号路径(AP 条 → 组合根 settle_batch——排雷玩法 D1×6① 回归载体)
main.ui.battle.board_zone.ap_bar._btn_stop.pressed.emit()
# 停手走真实点击路径(AP 条「停手」钮 → 组合根 settle_batch——排雷玩法 D1×6① 回归载体)
ProbeUiUtil.push_click(main, main.ui.battle.board_zone.ap_bar._btn_stop)
# ---- T1 D1:锁期/重放断言(每 run 一次,脚本化场景——演播期豁免的断言化承载) ----
+587
View File
@@ -0,0 +1,587 @@
# journey_player.gd — 全动线机器自玩驱动(C·发现环节去人化)
# 是什么:headless 场景驱动件——实例化 main、隔离存档,按旅程段驱动全动线(开机→设置→存档三操作
# 新档/删档/进档→营地功能窗逐窗开合→Esc 回退→大门开冒险→关卡推进→战斗→各停点→终局收束→回菜单),
# 每屏到达记账+段间异常上报,收尾落 JSONP 报告并入探针覆盖登记(真输入事件同源记账)。
# 判据(契约 §5):每个屏到达过+段间零 fatal。
# 异常上报三类(其余静默):①诊断收集器 fatal 级(settlement battle_result=fatal_error/fatal 屏)
# ②段间超时(单段 >30s 未达预期停点;战斗段另设状态无变化挂死阈值)③屏状态与预期不符。
# RID 泄漏等已知日志噪音静默。
# 归因字段:每条上报机器自填——段名/期望屏/实测屏/上一动作/通道标签(真实输入-按钮/
# 真实输入-场景热点/真实输入-键盘/方法-地图命中口/方法-棋盘口)。原归因制度残骸=本结构内字段。
# 误报收敛:误报指纹(类+段+归因摘要)落 .gates/coverage/journey_misfire_ledger.json——同一指纹
# 第 2 次起自动登记静默(计数照记、不再进上报清单);连 3 批零真缺陷判据收紧归用户复核轮。
# 驱动纪律:按钮/场景物件全走真输入(push_input 管线,同探针判据——ProbeUiUtil 单点通道)。
# 两处方法口如实标注通道:地图候选=press_candidate(自绘布局命中路径,探针族既定入口);
# 棋盘格=click_cell(BoardView 公共点击口,ui_input_probe 先例——棋盘管线面由该探针承载)。
# 停点分发的视图内部读数(_continue_btn/_node_pending 等)沿用探针族直读受控例外(iter53 反哺点 9 同款在册)。
# 运行:Godot_console.exe --headless --path . res://sim/journey_player.tscn
extends Node
const ProbeUiUtil := preload("res://probe/probe_ui_util.gd")
const ProbeCoverageRecorder := preload("res://probe/probe_coverage_recorder.gd")
const AutoPlayer := preload("res://sim/auto_player.gd")
const SEGMENT_TIMEOUT_MS := 30000 # 段间超时判据(契约 §5——单段 30s 未达预期停点)
const BATTLE_STALL_FRAMES := 90 # 战斗段状态无变化挂死阈值(演播期豁免同探针口径)
const ADVENTURE_BUDGET_FRAMES := 60000 # 冒险段帧预算(全停点循环兜底)
const MISFIRE_PATH := "res://tests/.gates/coverage/journey_misfire_ledger.json"
const REPORT_PATH := "res://tests/.gates/coverage/journey_report.js"
var main: Node
var _recorder: RefCounted = null
var _problems: Array = [] # 异常上报(三类;指纹二次起静默不入此列)
var _silent_count := 0 # 误报静默笔数(回执可见)
var _screens_seen: Dictionary = {} # 屏到达记账(判据面)
var _screen_trace: Array = [] # 屏到达变迁轨迹(诊断面——新屏首现时点+上一动作)
var _misfire: Dictionary = {} # 指纹 → 次数(跨跑落盘)
var _last_state := {} # 战斗段状态哈希(挂死检测)
var _stall := 0
var _last_action := "(无)" # 归因初判:上一动作描述
var _map_status_done := false # 地图屏状态角标一次性
var _legend_done := false # 图例开合一次性(每战斗面一次)
var _status_done := false # 战斗屏状态面板一次性
var _refresh_done := false # 刷新确认链一次性
func _ready() -> void:
await get_tree().process_frame
main = load("res://presentation/main.tscn").instantiate()
add_child(main)
await get_tree().process_frame
await get_tree().process_frame
_load_misfire()
_recorder = ProbeCoverageRecorder.new()
_recorder.attach(main, "journey")
ProbeUiUtil.isolate_save(main, "journey_player", false, false, true)
main.save_manager.slots.attach_slot_template("user://journey_player_slot_%d.json")
for i in range(1, 4): # 夹具清场——陈旧槽文件会翻有档态、新档钮缺席(open_camp_via_savesel 同款坑注)
for suf in ["", ".tmp", ".corrupt.bak"]:
var sp: String = main.save_manager.slots.slot_path(i) + suf
if FileAccess.file_exists(sp):
DirAccess.remove_absolute(sp)
main.ui.modals.menu.debug_overlay.set_sliders(400, 200) # 高加成保 L1 可完成(探针先例——夹具注)
main.ui.modals.menu.debug_overlay.seed_edit.text = "42"
await _phase_expect("开机落菜单", ["菜单"])
await _phase_menu_settings()
await _phase_new_slot(1)
await _phase_camp_windows()
await _phase_camp_esc_return()
await _phase_new_and_delete(2)
await _phase_load_slot(1)
await _phase_gate_start()
await _phase_adventure()
await _phase_end_back_menu()
_recorder.dump_js()
_dump_report()
_save_misfire()
var fatal := _fatal_count()
if _problems.is_empty() and fatal == 0 and _screens_seen.has("菜单") and _screens_seen.has("终局"):
print("[journey] PASS — 全动线走查完成(屏到达 %d 类,异常上报 0,fatal 0,误报静默 %d)" % [
_screens_seen.size(), _silent_count])
get_tree().quit(0)
return
print("[journey] FAIL — 异常 %d(静默 %d)/ fatal %d / 屏到达 %d 类" % [
_problems.size(), _silent_count, fatal, _screens_seen.size()])
for p in _problems:
print("[journey] - " + JSON.stringify(p, " "))
get_tree().quit(1)
# ---- 屏状态读取与记账 ----
func _screen_state() -> Dictionary:
var st := {
"菜单": main.ui.screens.start_overlay != null and main.ui.screens.start_overlay.visible,
"选档": main.ui.modals.menu.save_select_overlay.visible,
"设置": main.ui.modals.menu.settings_overlay.visible,
"生涯": main.ui.modals.menu.career_overlay.visible,
"确认弹窗": main.ui.modals.menu.confirm_dialog.is_open(),
"营地": main.ui.modals.camp.scene.visible,
"锻造窗": main.ui.modals.camp.forge.visible,
"树窗": main.ui.modals.camp.tree.visible,
"秘宝窗": main.ui.modals.camp.treasure.visible,
"图鉴窗": main.ui.modals.camp.codex.visible,
"Esc浮窗": main.ui.modals.camp.esc_overlay.visible,
"冒险": main.run_scope != null and main.run_scope.is_adventure,
"关卡屏": main.ui.modals.run.level_screen != null and main.ui.modals.run.level_screen.visible,
"三选一": main.ui.modals.run.curio_overlay.visible,
"商店": main.ui.modals.menu.shop_overlay.visible,
"大奖励": main.ui.screens.inter_overlay != null and main.ui.screens.inter_overlay.visible,
"节点": main.ui.modals.run.node_overlay.visible,
"终局": main.ui.modals.run.end_overlay != null and main.ui.modals.run.end_overlay.is_open(),
}
var in_battle: bool = bool(st["冒险"]) and not bool(st["关卡屏"]) and not bool(st["终局"]) \
and main.run_scope != null and main.run_scope.settlement != null \
and String(main.run_scope.settlement.battle_result) == ""
st["战斗"] = in_battle
return st
func _record_screens() -> void:
var st := _screen_state()
for k in st:
if bool(st[k]) and not _screens_seen.has(k):
_screens_seen[k] = true
_screen_trace.append({"t": Time.get_datetime_string_from_system(), "screen": k, "last_action": _last_action})
_check_exclusion(st)
var _excl_reported: Dictionary = {} # 屏互斥违约对(本跑一次性上报——跨跑由误报指纹静默)
# 屏级互斥(探针同款判据对):合法叠加仅「终局盖冒险」;违约=③屏状态与预期不符
func _check_exclusion(st: Dictionary) -> void:
var pairs := [["菜单", "营地"], ["菜单", "冒险"], ["营地", "冒险"], ["终局", "营地"], ["终局", "菜单"], ["菜单", "战斗"]]
for pair in pairs:
if bool(st.get(pair[0], false)) and bool(st.get(pair[1], false)):
var fk: String = str(pair[0]) + "×" + str(pair[1])
if not _excl_reported.has(fk):
_excl_reported[fk] = true
_report("screen_mismatch", "屏级互斥", "%s 与 %s 不应同场" % [pair[0], pair[1]])
func _active_screens() -> Array:
var on: Array = []
var st := _screen_state()
for k in st:
if bool(st[k]):
on.append(k)
return on
func _fatal_count() -> int:
var n := 0
if main.run_scope != null and main.run_scope.settlement != null \
and String(main.run_scope.settlement.battle_result) == "fatal_error":
n += 1
return n
# ---- 异常上报(三类+归因+误报收敛) ----
func _report(kind: String, seg: String, expect: String) -> void:
var finger_src := "%s|%s|%s|%s" % [kind, seg, expect, ",".join(_active_screens())]
var finger := str(finger_src.hash())
_misfire[finger] = int(_misfire.get(finger, 0)) + 1
if int(_misfire[finger]) >= 2:
_silent_count += 1 # 同一误报源第 2 次自动登记静默(契约 §5 收敛口径)
return
_problems.append({
"kind": kind, "segment": seg, "expect": expect,
"actual_screens": _active_screens(), "last_action": _last_action,
"fatal": _fatal_count(), "at": Time.get_datetime_string_from_system(),
})
func _load_misfire() -> void:
if FileAccess.file_exists(MISFIRE_PATH):
var parsed: Variant = JSON.parse_string(FileAccess.get_file_as_string(MISFIRE_PATH))
if parsed is Dictionary:
_misfire = parsed
func _save_misfire() -> void:
var f := FileAccess.open(MISFIRE_PATH, FileAccess.WRITE)
if f != null:
f.store_string(JSON.stringify(_misfire, " "))
f.close()
# ---- 段间等待(超时判据承载) ----
func _frames(n: int = 2) -> void:
for _i in n:
await get_tree().process_frame
_record_screens()
# 期望屏集合在超时内逐帧到达;到达即记账返回。超时=异常②超时+③屏状态不符双记(同源指纹静默收敛)。
func _phase_expect(seg: String, expect_keys: Array) -> void:
_last_action = "等待段「%s」落点(期望 %s)" % [seg, ",".join(expect_keys)]
var t0 := Time.get_ticks_msec()
while Time.get_ticks_msec() - t0 < SEGMENT_TIMEOUT_MS:
await get_tree().process_frame
_record_screens()
var st := _screen_state()
var all_on := true
for k in expect_keys:
all_on = all_on and bool(st.get(k, false))
if all_on:
await _frames(2) # 开层排版帧——当帧可见时容器未排,紧随的控件真点击会落空(探针件同款坑)
return
_report("timeout", seg, "屏 %s 应在场" % ",".join(expect_keys))
_report("screen_mismatch", seg, "屏 %s 应在场" % ",".join(expect_keys))
_save_misfire()
# ---- 旅程段 ----
func _phase_menu_settings() -> void:
# 设置开合(开始层真实入口——功能界面全覆盖义务)
var btn: Button = ProbeUiUtil.find_button(main.ui.screens.start_overlay, "设置", "prefix")
if btn == null:
_report("screen_mismatch", "设置入口", "开始层「设置」钮在场")
return
_last_action = "真点「设置」钮"
ProbeUiUtil.push_click(main, btn)
await _phase_expect("设置开", ["设置"])
var back: Button = ProbeUiUtil.find_button(main.ui.modals.menu.settings_overlay, "返回", "prefix")
if back != null:
_last_action = "真点设置「返回」钮"
ProbeUiUtil.push_click(main, back)
await _phase_expect("设置收", ["菜单"])
func _phase_new_slot(slot: int) -> void:
# 开机→存档·新档(选档层真实入口:进入存档→槽位新档→落营地)
var esb: Button = ProbeUiUtil.find_button(main.ui.screens.start_overlay, "进入存档", "prefix")
if esb == null:
_report("screen_mismatch", "选档入口", "开始层「进入存档」钮在场")
return
_last_action = "真点「进入存档」钮"
ProbeUiUtil.push_click(main, esb)
await _phase_expect("选档层开", ["选档"])
var nb: Button = main.ui.modals.menu.save_select_overlay._new_btns.get(slot)
if nb == null:
_report("screen_mismatch", "新档槽%d" % slot, "槽%d 新档钮在场" % slot)
return
_last_action = "真点槽%d「新档」钮" % slot
ProbeUiUtil.push_click(main, nb)
await _phase_expect("新档落营地", ["营地"])
func _phase_load_slot(slot: int) -> void:
var esb: Button = ProbeUiUtil.find_button(main.ui.screens.start_overlay, "进入存档", "prefix")
if esb == null:
_report("screen_mismatch", "选档入口", "开始层「进入存档」钮在场")
return
_last_action = "真点「进入存档」钮(进档链)"
ProbeUiUtil.push_click(main, esb)
await _phase_expect("选档层开", ["选档"])
var lb: Button = main.ui.modals.menu.save_select_overlay._load_btns.get(slot)
if lb == null:
_report("screen_mismatch", "进档槽%d" % slot, "槽%d 进档钮在场" % slot)
return
_last_action = "真点槽%d「进档」钮" % slot
ProbeUiUtil.push_click(main, lb)
await _phase_expect("进档落营地", ["营地"])
func _phase_new_and_delete(slot: int) -> void:
# 新档→篝火回菜单→选档删档(二次确认双路径真点击)——存档三操作收口
await _phase_new_slot(slot)
var camp: Control = main.ui.modals.camp.scene
_last_action = "真点篝火热点(回退动线)"
ProbeUiUtil.push_click_at(main, camp.get_hotspot_rect(&"campfire").get_center())
await _phase_expect("篝火确认弹窗", ["确认弹窗"])
var ok: Button = main.ui.modals.menu.confirm_dialog._confirm_btn
if ok != null:
_last_action = "真点篝火确认「确认」钮"
ProbeUiUtil.push_click(main, ok)
await _phase_expect("回菜单", ["菜单"])
# 删档:进选档→槽位删档一击开窗→真点取消→再删→真点确认(文件三态走查归探针既有套件——C 只验到达)
var esb: Button = ProbeUiUtil.find_button(main.ui.screens.start_overlay, "进入存档", "prefix")
if esb != null:
_last_action = "真点「进入存档」钮(删档链)"
ProbeUiUtil.push_click(main, esb)
await _phase_expect("选档层开", ["选档"])
var db: Button = main.ui.modals.menu.save_select_overlay._delete_btns.get(slot)
if db != null:
_last_action = "真点槽%d「删档」钮一击" % slot
ProbeUiUtil.push_click(main, db)
await _phase_expect("删档确认窗", ["确认弹窗"])
var cc: Button = main.ui.modals.menu.confirm_dialog._cancel_btn
if cc != null:
_last_action = "真点删档「取消」钮"
ProbeUiUtil.push_click(main, cc)
await _frames(2)
_last_action = "真点槽%d「删档」钮二击" % slot
ProbeUiUtil.push_click(main, db)
await _phase_expect("删档确认窗", ["确认弹窗"])
var ok2: Button = main.ui.modals.menu.confirm_dialog._confirm_btn
if ok2 != null:
_last_action = "真点删档「确认」钮"
ProbeUiUtil.push_click(main, ok2)
await _frames(3)
var back: Button = ProbeUiUtil.find_button(main.ui.modals.menu.save_select_overlay, "返回", "prefix")
if back != null:
_last_action = "真点选档「返回」钮"
ProbeUiUtil.push_click(main, back)
await _phase_expect("选档收", ["菜单"])
func _phase_camp_windows() -> void:
# 营地功能窗逐窗真点开合(锻造/秘宝/树/图鉴/生涯——campfire/gate 归动线段)
var views := {
&"forge": ["锻造窗", main.ui.modals.camp.forge],
&"chest": ["秘宝窗", main.ui.modals.camp.treasure],
&"dummy": ["树窗", main.ui.modals.camp.tree],
&"board": ["图鉴窗", main.ui.modals.camp.codex],
&"notebook": ["生涯", main.ui.modals.menu.career_overlay],
}
for key: StringName in [&"forge", &"chest", &"dummy", &"board", &"notebook"]:
var camp: Control = main.ui.modals.camp.scene
_last_action = "真点营地物件热点 %s" % String(key)
ProbeUiUtil.push_click_at(main, camp.get_hotspot_rect(key).get_center())
await _phase_expect("%s窗开" % String(key), [views[key][0]])
var back: Button = ProbeUiUtil.find_button(views[key][1], "返回营地")
if back == null:
back = ProbeUiUtil.find_button(views[key][1], "返回", "prefix")
if back != null:
_last_action = "真点 %s 窗返回钮" % String(key)
ProbeUiUtil.push_click(main, back)
await _frames(3)
func _phase_camp_esc_return() -> void:
# 营地 Esc 浮窗→「返回主菜单」真点(回退动线+浮窗功能面到达)
_push_esc()
await _phase_expect("Esc浮窗开", ["Esc浮窗"])
var rb: Button = main.ui.modals.camp.esc_overlay._return_btn
if rb != null:
_last_action = "真点浮窗「返回主菜单」钮"
ProbeUiUtil.push_click(main, rb)
await _phase_expect("浮窗回菜单", ["菜单"])
func _phase_gate_start() -> void:
# 大门确认→开冒险(动线收口——确认后落点=冒险,非回菜单)
var camp: Control = main.ui.modals.camp.scene
_last_action = "真点大门热点"
ProbeUiUtil.push_click_at(main, camp.get_hotspot_rect(&"gate").get_center())
await _phase_expect("大门确认弹窗", ["确认弹窗"])
var ok: Button = main.ui.modals.menu.confirm_dialog._confirm_btn
if ok != null:
_last_action = "真点大门「确认」钮"
ProbeUiUtil.push_click(main, ok)
await _phase_expect("冒险发起", ["冒险"])
# 已知缺陷现场(发现环义务已尽):大门开局后开始层滞留盖屏=「开局被开始层盖住」
# (营地交互修复批承接中、合回前 master 在案)——开始层可见直入 locks_input 谓词,战斗全程锁死。
# C 上报后以夹具面隐藏开始层继续走查(通道=夹具修整,非真输入;缺陷本体不因此掩盖)。
await _frames(2)
if main.ui.screens.start_overlay != null and main.ui.screens.start_overlay.visible:
_report("screen_mismatch", "大门开局", "开始层滞留盖屏(已知缺陷——营地交互修复批承接中)")
_last_action = "夹具修整:隐藏滞留开始层以解锁战斗走查(缺陷已上报在案)"
main.ui.screens.start_overlay.visible = false
await _frames(2)
func _phase_adventure() -> void:
# 关卡推进段:入口选路→战斗→各停点分发→真终局收口(帧预算兜底)
var budget := ADVENTURE_BUDGET_FRAMES
while budget > 0:
budget -= 1
await get_tree().process_frame
_record_screens()
var st := _screen_state()
if bool(st["节点"]):
_last_action = "节点层「继续」(方法-视图口)"
main.ui.modals.run.node_overlay.press_continue()
continue
if bool(st["三选一"]):
_drive_curio()
continue
if bool(st["商店"]):
var lv: Button = ProbeUiUtil.find_button(main.ui.modals.menu.shop_overlay, "离开商店")
if lv != null:
_last_action = "真点「离开商店」钮"
ProbeUiUtil.push_click(main, lv)
continue
if bool(st["大奖励"]):
var nb: Button = ProbeUiUtil.find_button(main.ui.screens.inter_overlay, "进入下一层", "prefix")
if nb != null:
_last_action = "真点大奖励「进入下一层」钮"
ProbeUiUtil.push_click(main, nb)
await _frames(2)
continue
if bool(st["关卡屏"]):
_drive_map_once()
continue
if bool(st["战斗"]):
await _drive_battle_once()
continue
var endo: Control = main.ui.modals.run.end_overlay
if endo != null and endo.is_open():
var armed: bool = main.run_scope.settlement_stop_timer.is_armed()
var running: bool = main.run_scope.is_adventure and main.run_scope.adventure != null \
and String(main.run_scope.adventure.state) == "running"
if armed or running:
_last_action = "真点结算面「继续/下一节点」钮"
ProbeUiUtil.push_click(main, main.ui.modals.run.end_overlay._btn)
await _frames(2)
continue
break # 真终局(失败/放弃/打穿)——收口段交棒
if budget <= 0:
_last_action = "冒险段超时(诊断:%s)" % JSON.stringify({
"pacer_pending": main.ui.screens.row_pacer.pending_rows().size(),
"locked": main.ui.flow.locks_input(),
"battle_result": String(main.run_scope.settlement.battle_result) if main.run_scope.settlement != null else "?",
"level": int(main.run_scope.runner.level_row.get("level_index", -1)) if main.run_scope.runner != null else -1,
"round": int(main.run_scope.settlement.round_index) if main.run_scope.settlement != null else -1,
"ap": int(main.run_scope.settlement.action_economy.ap) if main.run_scope.settlement != null else -1,
})
_report("timeout", "冒险段", "帧预算耗尽未达真终局")
_save_misfire()
func _drive_map_once() -> void:
# 地图屏:状态角标真点开合一次+候选命中(方法-地图命中口——自绘布局路径,探针族同款入口)
if not _map_status_done and main.ui.modals.run.level_screen.status_badge != null \
and not main.ui.modals.run.level_screen.status_badge.disabled:
_map_status_done = true
var badge: Button = main.ui.modals.run.level_screen.status_badge
_last_action = "真点地图屏状态角标"
ProbeUiUtil.push_click(main, badge)
await _frames(3) # 面板开层排版帧——同帧点「继续」会落空、输入锁滞留全战役(实证)
var resumed: Button = ProbeUiUtil.find_button(main.ui.modals.run.status_panel, "继续", "prefix")
if resumed != null and main.ui.modals.run.status_panel.visible:
ProbeUiUtil.push_click(main, resumed)
_last_action = "地图候选命中(方法-地图命中口)"
main.ui.modals.run.level_screen.press_candidate(0)
func _drive_curio() -> void:
# 三选一停点:待选态点首件+确认;兑付/已消费态点继续(方法-视图口——curio 停点既定驱动面)
var co: Control = main.ui.modals.run.curio_overlay
_last_action = "三选一停点分发(方法-视图口)"
if co._continue_btn.visible:
co.press_continue()
elif main.run_scope.adventure != null and not main.run_scope.adventure.stop()._node_pending.is_empty():
co.press_card(0)
co.press_confirm()
elif main.run_scope.adventure != null and not main.run_scope.adventure.stop()._pending.is_empty():
co.press_card(0)
co.press_confirm()
else:
co.press_continue()
func _drive_battle_once() -> void:
# 战斗单步:挂死检测+功能钮真点(图例/状态/刷新确认链/停手)+棋盘挖格(方法-棋盘口)
var settlement = main.run_scope.settlement
var diag: Dictionary = settlement.diagnostic_snapshot()
var st := {
"round": diag.round, "phase": diag.action_economy.phase, "ap": diag.action_economy.ap,
"revealed": diag.minesweeper_revealed,
}
var pacer_idle: bool = main.ui.screens.row_pacer.pending_rows().is_empty()
if JSON.stringify(st) == JSON.stringify(_last_state) and pacer_idle \
and not main.ui.flow.locks_input():
_stall += 1
if _stall >= BATTLE_STALL_FRAMES:
_report("timeout", "战斗段", "状态无变化达 %d 帧" % BATTLE_STALL_FRAMES)
_stall = 0
_save_misfire()
_last_state = {} # 重置基线防同帧连报
else:
_stall = 0
_last_state = st
if not pacer_idle or main.ui.flow.locks_input():
return
var ap_bar: Control = main.ui.battle.board_zone.ap_bar
# 功能钮真点(一次性——图例开合/状态面板开合/刷新确认链)
if not _legend_done:
_legend_done = true
_last_action = "真点图例钮"
ProbeUiUtil.push_click(main, ap_bar._btn_legend)
await _frames(2)
var lclose: Button = ProbeUiUtil.find_button(main.ui.modals.run.legend_overlay, "关闭")
if lclose != null and main.ui.modals.run.legend_overlay.is_open():
ProbeUiUtil.push_click(main, lclose)
await _frames(2)
return
if not _status_done and not main.ui.battle.board_zone.status_badge.disabled:
_status_done = true
_last_action = "真点战斗屏状态角标"
ProbeUiUtil.push_click(main, main.ui.battle.board_zone.status_badge)
await _frames(3) # 面板开层排版帧(同地图屏实证坑)
var resumed: Button = ProbeUiUtil.find_button(main.ui.modals.run.status_panel, "继续", "prefix")
if resumed != null and main.ui.modals.run.status_panel.visible:
ProbeUiUtil.push_click(main, resumed)
await _frames(2)
return
if not _refresh_done and ap_bar.is_refresh_ready():
_refresh_done = true
_last_action = "真点「弃图刷新」钮+确认链"
ProbeUiUtil.push_click(main, ap_bar._btn_refresh)
await _frames(2)
var cbtn: Button = ProbeUiUtil.find_button(ap_bar, "确认弃图")
if cbtn != null:
ProbeUiUtil.push_click(main, cbtn)
await _frames(2)
return
# 棋盘动作(方法-棋盘口——BoardView 公共点击口,ui_input_probe 先例;C 的按钮面全走真输入)
var minesweeper = settlement.minesweeper
var plan: Dictionary = AutoPlayer.choose_dig(minesweeper)
if plan.is_empty():
plan = AutoPlayer.gamble_pick(minesweeper)
if plan.is_empty():
_last_action = "真点「停手」钮(无计划)"
ProbeUiUtil.push_click(main, ap_bar._btn_stop)
await _frames(2)
return
_last_action = "棋盘挖格 %s(方法-棋盘口)" % str(plan.dig_cell)
if plan.hero_cell != minesweeper.hero_pos():
main.ui.battle.board_zone.board_view.click_cell(plan.hero_cell, MOUSE_BUTTON_LEFT)
main.ui.battle.board_zone.board_view.click_cell(plan.dig_cell, MOUSE_BUTTON_LEFT)
await _frames(1)
func _phase_end_back_menu() -> void:
# 终局收束:真点「返回」→ 回菜单;结算框/中途面已在冒险段分发
var t0 := Time.get_ticks_msec()
while Time.get_ticks_msec() - t0 < SEGMENT_TIMEOUT_MS:
await get_tree().process_frame
_record_screens()
var endo: Control = main.ui.modals.run.end_overlay
if endo != null and endo.is_open() and not main.run_scope.settlement_stop_timer.is_armed():
break
if main.ui.modals.run.end_overlay == null or not main.ui.modals.run.end_overlay.is_open():
_report("timeout", "终局收束", "真终局面未在超时内到达")
_save_misfire()
return
var btn: Button = main.ui.modals.run.end_overlay._btn
if btn != null:
_last_action = "真点终局「返回」钮"
ProbeUiUtil.push_click(main, btn)
await _phase_expect("终局回菜单", ["菜单"])
func _push_esc() -> void:
var ev := InputEventKey.new()
ev.keycode = KEY_ESCAPE
ev.physical_keycode = KEY_ESCAPE
ev.pressed = true
main.get_viewport().push_input(ev, true) # 键盘真输入(通道=真实输入-键盘)
# ---- 报告落盘(JSONP) ----
func _dump_report() -> void:
var dir := DirAccess.open("res://")
if dir != null:
dir.make_dir_recursive("tests/.gates/coverage")
var payload := {
"meta": {
"generated_at": Time.get_datetime_string_from_system(),
"seed": OS.get_environment("PROBE_SEED"),
"screens_seen_total": _screens_seen.size(),
"anomalies_reported": _problems.size(),
"anomalies_silent": _silent_count,
"fatal_count": _fatal_count(),
"anomaly_classes": "fatal/timeout/screen_mismatch(其余静默;误报指纹二次起静默)",
},
"screens_seen": _screens_seen.keys(),
"screen_trace": _screen_trace,
"anomalies": _problems,
}
var f := FileAccess.open(REPORT_PATH, FileAccess.WRITE)
if f != null:
var js := "/* 全动线自玩报告——journey_player.gd 随跑覆写;误报核销归用户复核轮。 */\n"
js += "window.__QA_COVERAGE__ = window.__QA_COVERAGE__ || {};\n"
js += "window.__QA_COVERAGE__.journey = %s;\n" % JSON.stringify(payload, " ")
f.store_string(js)
f.close()
print("[journey] 报告已落盘 %s(屏到达 %d 类 / 上报 %d / 静默 %d)" % [
REPORT_PATH, _screens_seen.size(), _problems.size(), _silent_count])
+6
View File
@@ -0,0 +1,6 @@
[gd_scene load_steps=2 format=3 uid="uid://cjourneyplayer1"]
[ext_resource type="Script" path="res://sim/journey_player.gd" id="1"]
[node name="JourneyPlayer" type="Node"]
script = ExtResource("1")
+3 -1
View File
@@ -1,9 +1,11 @@
# 本目录只入库 baseline.json(存量基线,需随代码走以保证各 worktree 判定口径一致)。
# receipt.json 是每跑批覆写的当次回执、ci-status.json/ci-history.jsonl 是已退役的 CI 回执判定与流水账(判定只在远端,本机残留)、
# ci.lock/ci.rerun 是跑批件锁与补跑标记、*.log 是跑批当场日志——均为中间产物,不入库。
# ci.lock/ci.rerun 是跑批件锁与补跑标记、*.log 是跑批当场日志、coverage/ 是探针真输入覆盖登记
#(JSONP 式 .js,每次探针运行自动覆写——观测产物非事实源,不入库)——均为中间产物,不入库。
receipt.json
ci-status.json
ci-history.jsonl
ci.lock
ci.rerun
*.log
coverage/
@@ -0,0 +1,62 @@
# QA harness 三批——缺陷与覆盖遗漏报告(2026-11-06)
> **接收方**:待办清单「QA harness 三批缺陷与覆盖遗漏收口」条目(P0)。
> **来源**:合并 d3c85390(批 1 探针真输入化+覆盖登记/批 2 覆盖总览面板+本体枚举器/批 3 自动试玩扩展)三批实施过程的真实跑批读数。
> **口径**:本报告只列**发现且未解决**的缺陷与测试遗漏(实施期已自修的环境/机制坑不在列,见 qa-harness 讨论文档 §10–§12);明细可查出处逐条标注。
## 一、产品缺陷(2 类)
### A1 开局被开始层盖住(大门开局开始层滞留盖屏+输入锁死)
- **现象**:大门确认开局冒险后,开始层(start_overlay)滞留可见;它以栈内模态身份直入输入锁谓词(`game/presentation/modal_flow.gd:77` 的 `start_visible_get` 项),战斗全程 `locks_input()==true`,玩家无法操作。
- **发现**:C 自动试玩首跑(journey_player)即机器复现——上报 2 条(同缺陷双指纹),归因字段在案(`game/tests/.gates/coverage/journey_report.js`)。
- **现状**:**营地交互修复批已修未合**(camp-interact 分支 0977a548,main.gd 大门两出口补隐藏句)——该分支合回 master 后本缺陷即消,C 的对应上报自然消失。
- **归属**:随 camp-interact 合回即消,无需新批。
### A2 终局面板关闭路径引擎报错噪声
- **现象**:`game/presentation/views/battle_end_overlay.gd:194` 的 `get_meta(&"celebrate_tw", null)` 在 meta 缺失时仍触发引擎 SCRIPT ERROR 打印(当前引擎版对显式 null 缺省值照样报错);关闭动线功能不受影响,属持续输出的日志噪声。
- **发现**:批 1 探针直跑日志在案(screen_flow_probe 终局段每次触发)。
- **修法**(一行级,未实施):改 `has_meta()` 前置判断。归属=需求性工程条目(改游戏行为),走写卡通道。
## 二、测试覆盖遗漏(6 类)
### B1 停手钮(AP 条)零真输入覆盖
- **现象**:两探针与 C 均未以真输入点过「停手」钮——emit 时代信号直发绕过 disabled 掩盖了驱动空洞;停手使能位=alive∧digging 投影(`game/presentation/views/ap_bar_view.gd:147`、`hud_presenter.gd:92`)。
- **登记处**:覆盖面板控件下钻(ui_click 探针 seen 91/命中 27 的缺口之一);`probe_hits_ui_click.js` 零停手键。
### B2 视图层测试方法面未真输入化
- **现象**:弃图刷新确认链(press_refresh_for_probe/press_confirm_for_probe)、地图候选(press_candidate)、节点与三选一停点(press_continue/press_card/press_confirm)等驱动走视图方法口,非输入管线——保「逻辑通」不保「玩家点击可达」。
- **登记处**:待办清单「测试面真输入化分期与存量断言逐条精锚」条目①余项;C 的两方法口在归因字段如实标注通道。
### B3 存量 996 条断言判据出处精锚
- **现象**:链式锚已保可核(capability_audit 链断 0),逐条出处粒度精锚未做。
- **登记处**:同上条目②,原样在案。
### B4 本体对账缺口 212 条/62 类
- **现象**:游戏本体实体(data/tables 行集、schema 词表与字段、场景按钮、自绘命中类、屏模态清单,共 865 实体)在测试断言文料中零关联 212 条——「游戏做了但从未被验证」的候选面。
- **诚实边界**:grep 关联有假阳假阴(断言测了实体但文本没带 id=假缺口;文本带 id 但断言弱=假覆盖)——**须人工逐条复核后再立事项**,禁直接照单开工。
- **登记处**:覆盖面板第四节(`tools/coverage_report.html`);`game/tests/.gates/coverage/ontology_gaps.js`。
### B5 控件级零真输入命中缺口(ui_click 64 处/screen_flow 97 处)
- **现象**:探针真输入登记的「存在但零命中」控件缺口——大头=未被探针打开过的屏(完备性边界在案:控件级视图只对被打开过的屏负责),其中混有 B1 类真空洞。
- **登记处**:覆盖面板第三节;`probe_hits_ui_click.js`/`probe_hits_screen_flow.js` 的 seen−hit 差集。
### B6 C 判据收紧轮未做
- **现象**:全三关推进约 8 分钟(冒险段帧预算 60000 兜底);契约条款「连 3 批零真缺陷→判据收紧一轮」的触发权与执行归用户复核轮。
- **登记处**:`journey_report.js` meta(上报数/静默数/fatal/屏到达进回执)+qa-harness 讨论文档 §5。
## 三、处置归属(汇总)
| 组 | 项 | 通道 |
|---|---|---|
| 随合即消 | A1 | camp-interact 分支合回,无需新批 |
| 写卡通道 | A2、B1、B2 | 需求性工程条目(改游戏行为/测试驱动)→ 需求卡片 |
| 既有义务 | B3、B6 | 衔接条目②+用户复核轮 |
| 人工复核 | B4、B5 | 面板逐条复核后按性质分流(真缺口立事项/假缺口面板豁免清单核销) |
@@ -8,9 +8,9 @@
| 项 | 判定 | 说明 |
|---|---|---|
| D-dungeon | PASS | 铺底代表值门(L/饱和/禁入带;近色只报读数):RGB=(181, 155, 119) L=0.346 饱和=0.343 色相=34.8° 近色对=4(在役素材色,非新增) |
| D-underdark | PASS | 铺底代表值门(L/饱和/禁入带;近色只报读数):RGB=(186, 193, 210) L=0.532 饱和=0.114 色相=222.5° 近色对=4(在役素材色,非新增) |
| D-deep_underdark | PASS | 铺底代表值门(L/饱和/禁入带;近色只报读数):RGB=(150, 158, 186) L=0.345 饱和=0.194 色相=226.7° 近色对=9(在役素材色,非新增) |
| D-dungeon | PASS | 铺底代表值门(底型瓦片实测顶面受光档;亮度/饱和/禁入带,近色只报读数):RGB=(180, 144, 105) L=0.307 饱和=0.417 色相=31.2° 近色对=13(在役素材色,非新增) |
| D-underdark | PASS | 铺底代表值门(底型瓦片实测顶面受光档;亮度/饱和/禁入带,近色只报读数):RGB=(186, 193, 210) L=0.532 饱和=0.114 色相=222.5° 近色对=4(在役素材色,非新增) |
| D-deep_underdark | PASS | 铺底代表值门(底型瓦片实测顶面受光档;亮度/饱和/禁入带,近色只报读数):RGB=(150, 158, 186) L=0.345 饱和=0.194 色相=226.7° 近色对=9(在役素材色,非新增) |
| A-stair-dungeon-素砖 | PASS | 明度阶梯单调递减:165.242→145.992→130.196→114.266→92.362 |
| A-stair-dungeon-裂纹 | PASS | 明度阶梯单调递减:165.242→145.992→130.196→114.266→92.362 |
| A-stair-dungeon-碎石斑 | PASS | 明度阶梯单调递减:164.957→144.992→129.269→113.266→91.434 |
@@ -26,11 +26,11 @@
| GRAY | PASS | 灰度副本段零彩色像素:命中 0 |
| EDGE | PASS | 四缘 10px 带内零内容像素:命中 0 |
## S1 机检 D:铺底代表值(H2 槽)五门
## S1 机检 D:铺底代表值(底型瓦片实测顶面受光档)五门
| 关 | RGB | L(线性域) | 饱和 | 色相 | 近色对 | 判定 |
|---|---|---|---|---|---|---|
| dungeon | (181, 155, 119) | 0.346 | 0.343 | 34.8° | 4 | 过 |
| dungeon | (180, 144, 105) | 0.307 | 0.417 | 31.2° | 13 | 过 |
| underdark | (186, 193, 210) | 0.532 | 0.114 | 222.5° | 4 | 过 |
| deep_underdark | (150, 158, 186) | 0.345 | 0.194 | 226.7° | 9 | 过 |
@@ -38,21 +38,21 @@
| 关 | 底型 | 微光组 | 带内合成均值 | 格心素底均值 | 模型带内均值 | 模型差 | 三通道差 | 色相差° | 判定 |
|---|---|---|---|---|---|---|---|---|---|
| dungeon | 素砖 | BUFF | (193, 166, 76) | (145, 116, 84) | (193, 166, 76) | 0 | [48, 50, 8] | 17 | 过 |
| dungeon | 素砖 | HEAL | (126, 159, 76) | (145, 116, 84) | (126, 159, 75) | 1 | [19, 43, 8] | 69 | 过 |
| dungeon | 素砖 | PIERCE | (100, 140, 122) | (145, 116, 84) | (100, 140, 122) | 0 | [45, 24, 38] | 145 | 过 |
| dungeon | 素砖 | MAGIC | (135, 105, 145) | (145, 116, 84) | (135, 105, 145) | 0 | [10, 11, 61] | 139 | 过 |
| dungeon | 素砖 | AOE | (155, 76, 70) | (145, 116, 84) | (155, 76, 70) | 0 | [10, 40, 14] | 46 | 过 |
| dungeon | 裂纹 | BUFF | (193, 166, 76) | (143, 115, 83) | (193, 166, 76) | 0 | [50, 51, 7] | 17 | 过 |
| dungeon | 裂纹 | HEAL | (126, 159, 76) | (143, 115, 83) | (126, 159, 76) | 0 | [17, 44, 7] | 69 | 过 |
| dungeon | 裂纹 | PIERCE | (100, 140, 122) | (143, 115, 83) | (100, 140, 122) | 0 | [43, 25, 39] | 145 | 过 |
| dungeon | 裂纹 | MAGIC | (135, 105, 145) | (143, 115, 83) | (135, 105, 145) | 0 | [8, 10, 62] | 139 | 过 |
| dungeon | 裂纹 | AOE | (155, 76, 70) | (143, 115, 83) | (155, 76, 70) | 0 | [12, 39, 13] | 46 | 过 |
| dungeon | 碎石斑 | BUFF | (192, 166, 75) | (147, 118, 85) | (192, 166, 75) | 0 | [45, 48, 10] | 17 | 过 |
| dungeon | 碎石斑 | HEAL | (125, 158, 75) | (147, 118, 85) | (125, 159, 75) | 1 | [22, 40, 10] | 69 | 过 |
| dungeon | 碎石斑 | PIERCE | (99, 139, 122) | (147, 118, 85) | (99, 139, 122) | 0 | [48, 21, 37] | 145 | 过 |
| dungeon | 碎石斑 | MAGIC | (134, 104, 144) | (147, 118, 85) | (134, 104, 144) | 0 | [13, 14, 59] | 139 | 过 |
| dungeon | 碎石斑 | AOE | (154, 75, 70) | (147, 118, 85) | (154, 75, 70) | 0 | [7, 43, 15] | 46 | 过 |
| dungeon | 素砖 | BUFF | (193, 166, 76) | (145, 116, 84) | (193, 166, 76) | 0 | [48, 50, 8] | 21 | 过 |
| dungeon | 素砖 | HEAL | (126, 159, 76) | (145, 116, 84) | (126, 159, 75) | 1 | [19, 43, 8] | 73 | 过 |
| dungeon | 素砖 | PIERCE | (100, 140, 122) | (145, 116, 84) | (100, 140, 122) | 0 | [45, 24, 38] | 149 | 过 |
| dungeon | 素砖 | MAGIC | (135, 105, 145) | (145, 116, 84) | (135, 105, 145) | 0 | [10, 11, 61] | 136 | 过 |
| dungeon | 素砖 | AOE | (155, 76, 70) | (145, 116, 84) | (155, 76, 70) | 0 | [10, 40, 14] | 43 | 过 |
| dungeon | 裂纹 | BUFF | (193, 166, 76) | (143, 115, 83) | (193, 166, 76) | 0 | [50, 51, 7] | 21 | 过 |
| dungeon | 裂纹 | HEAL | (126, 159, 76) | (143, 115, 83) | (126, 159, 76) | 0 | [17, 44, 7] | 73 | 过 |
| dungeon | 裂纹 | PIERCE | (100, 140, 122) | (143, 115, 83) | (100, 140, 122) | 0 | [43, 25, 39] | 149 | 过 |
| dungeon | 裂纹 | MAGIC | (135, 105, 145) | (143, 115, 83) | (135, 105, 145) | 0 | [8, 10, 62] | 136 | 过 |
| dungeon | 裂纹 | AOE | (155, 76, 70) | (143, 115, 83) | (155, 76, 70) | 0 | [12, 39, 13] | 43 | 过 |
| dungeon | 碎石斑 | BUFF | (192, 166, 75) | (147, 118, 85) | (192, 166, 75) | 0 | [45, 48, 10] | 21 | 过 |
| dungeon | 碎石斑 | HEAL | (125, 158, 75) | (147, 118, 85) | (125, 159, 75) | 1 | [22, 40, 10] | 73 | 过 |
| dungeon | 碎石斑 | PIERCE | (99, 139, 122) | (147, 118, 85) | (99, 139, 122) | 0 | [48, 21, 37] | 149 | 过 |
| dungeon | 碎石斑 | MAGIC | (134, 104, 144) | (147, 118, 85) | (134, 104, 144) | 0 | [13, 14, 59] | 136 | 过 |
| dungeon | 碎石斑 | AOE | (154, 75, 70) | (147, 118, 85) | (154, 75, 70) | 0 | [7, 43, 15] | 43 | 过 |
| underdark | 素砖 | BUFF | (172, 164, 100) | (104, 110, 128) | (172, 164, 100) | 0 | [68, 54, 28] | 170 | 过 |
| underdark | 素砖 | HEAL | (105, 157, 100) | (104, 110, 128) | (105, 157, 100) | 0 | [1, 47, 28] | 119 | 过 |
| underdark | 素砖 | PIERCE | (78, 137, 147) | (104, 110, 128) | (78, 137, 147) | 0 | [26, 27, 19] | 42 | 过 |
@@ -183,4 +183,21 @@
机检 17 项,FAIL 0 项。
目检(S3)归用户:四条判据见方案 §2.2(逐对指认 45/45/灰度副本 45/45/相邻并列 36 对可辨/装饰不抢读)。
目检(S3)归用户,判定见下节。
## S3 目检判定(用户在会话中判定,2026-11-06)
| 条 | 判据(方案 §2.2) | 判定 |
|---|---|---|
| 1 | 正图段 45 格逐格指认微光属哪一组(五选一),45/45 | 过 |
| 2 | 灰度副本段(Rec.709 去色=色弱通道)重做第 1 条,45/45 | 过 |
| 3 | 阶梯序相邻 36 对逐对判可分辨,全对可分 | 过 |
| 4 | 裂纹/碎石斑两种装饰变体底上 30 格:微光仍可指认且装饰不成第二焦点 | 过 |
判定口径=用户回话「没有问题,我觉得是可以分清楚的」,经追问四条覆盖范围后答「四条全过」。
## S4 抽测结论
抽测通过:机检 17 项 FAIL 0 + 目检四条全过。
同批生效的口径订正:铺底代表值改为从三块底型瓦片像素实测(线性域相对亮度取大),不再引用换色脚本的锚槽表值;dungeon 实测 `#B49069`(L=0.307,过门余量 0.007)。样图逐字节未变(sha256 `15d33fabb752566772540f436ca992fc71b0074fd5da71ff398a9b6bbb789087`,与 2026-11-05 出图件一致),故 S3 目检结论继续有效。
+158
View File
@@ -0,0 +1,158 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
r"""coverage_enum.py — 本体枚举器(游戏内容↔测试验证对账,B 面板·义务层双源之机器源)
两个文本扫描器+一次比对,全程不依赖跑游戏(交互命中维度除外——由探针覆盖登记另行承载):
扫描器 1(游戏本体枚举)——「游戏有什么」:
game/data/tables/*.tres 行集主键(`"xxx_id": "yyy"` 形态,逐表逐行)
game/data/schemas/*.gd 词表常量与字段名(const 块内的引号 token+var 字段名)
game/presentation/**/*.tscn BaseButton 族节点(scene+节点名)
game/presentation/**/*.gd _gui_input/_input 自绘命中类(脚本名)
game/presentation/views/*.gd 屏与模态清单(*_screen/*_overlay/*_panel 视图类)
扫描器 2(验证关联)——「验证了什么」:复用 tools/extract_checks.py 的断言静态提取
(unit/**+integration/**+system_test.gd 全量),实体 id 在「断言标签+cond+源文件+函数」
文料中出现即记一次关联。
比对=差集:本体存在、验证零关联=缺口清单,按类别分组、标首见批次(本工具生成时间)。
诚实边界(契约 §7 在案):grep 关联有假阳假阴——断言测了实体但文本没带 id=假缺口(人工核销
兜);文本带 id 但断言弱=假覆盖(oracle 改造堵)。推荐约定「断言文本含被测实体 id」提高精度,
不立规约。交互点(按钮/物件被点过没有)不在本工具判据内——那归探针真输入覆盖登记。
用法: python tools/coverage_enum.py [--out <路径>]
产出: game/tests/.gates/coverage/ontology_gaps.js(JSONP,B 面板消费)
"""
from __future__ import annotations
import argparse
import datetime
import json
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
GAME = ROOT / "game"
DEFAULT_OUT = GAME / "tests" / ".gates" / "coverage" / "ontology_gaps.js"
TRES_ID_RE = re.compile(r'"(\w+_id)":\s*"([^"]+)"')
SCHEMA_TOKEN_RE = re.compile(r'"([a-z][a-z0-9_]{2,40})"')
SCHEMA_VAR_RE = re.compile(r"^@?export\s+var\s+(\w+)\s*:", re.M)
TSCN_NODE_RE = re.compile(r'\[node name="([^"]+)"[^>]*type="(\w*Button\w*)"')
GUI_INPUT_RE = re.compile(r"^func (_gui_input|_input)\(", re.M)
VIEW_KIND_RE = re.compile(r"(screen|overlay|panel)$")
def scan_tables() -> list[dict]:
out: list[dict] = []
tables = GAME / "data" / "tables"
for f in sorted(tables.glob("*.tres")):
text = f.read_text(encoding="utf-8", errors="replace")
seen: set[tuple[str, str]] = set()
for m in TRES_ID_RE.finditer(text):
key = (m.group(1), m.group(2))
if key not in seen:
seen.add(key)
out.append({"category": "表行:%s" % f.stem, "id": m.group(2), "id_field": m.group(1)})
return out
def scan_schemas() -> list[dict]:
out: list[dict] = []
schemas = GAME / "data" / "schemas"
for f in sorted(schemas.glob("*.gd")):
text = f.read_text(encoding="utf-8", errors="replace")
tokens = set(SCHEMA_TOKEN_RE.findall(text))
fields = set(SCHEMA_VAR_RE.findall(text))
for t in sorted(tokens):
out.append({"category": "词表/枚值:%s" % f.stem, "id": t, "id_field": "token"})
for fl in sorted(fields):
out.append({"category": "schema字段:%s" % f.stem, "id": fl, "id_field": "field"})
return out
def scan_tscn_buttons() -> list[dict]:
out: list[dict] = []
for f in sorted(GAME.glob("presentation/**/*.tscn")):
text = f.read_text(encoding="utf-8", errors="replace")
for m in TSCN_NODE_RE.finditer(text):
out.append({"category": "场景按钮", "id": "%s:%s" % (f.name, m.group(1)), "id_field": m.group(2)})
return out
def scan_gui_input_classes() -> list[dict]:
out: list[dict] = []
for f in sorted(GAME.glob("presentation/**/*.gd")):
text = f.read_text(encoding="utf-8", errors="replace")
kinds = set(GUI_INPUT_RE.findall(text))
for k in kinds:
out.append({"category": "自绘命中类", "id": f.stem, "id_field": k})
break # 一类一行(一个脚本同具两面也只记一次)
return out
def scan_screens() -> list[dict]:
out: list[dict] = []
for f in sorted((GAME / "presentation" / "views").glob("*.gd")):
if VIEW_KIND_RE.search(f.stem):
out.append({"category": "屏与模态", "id": f.stem, "id_field": "view"})
return out
def assertion_corpus() -> tuple[list[dict], str]:
sys.path.insert(0, str(ROOT / "tools"))
import extract_checks as ec # noqa: PLC0415 —— 复用现役提取机制(契约 §7 指定扩面)
rows, _problems = ec.extract_all(ec.default_sources())
parts: list[str] = []
for r in rows:
parts.append("%s %s %s %s" % (r.get("src", ""), r.get("func", ""),
r.get("label_literal", ""), r.get("cond", "")))
return rows, "\n".join(parts)
def main() -> int:
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
ap = argparse.ArgumentParser(description="本体枚举器(游戏内容↔测试验证对账)")
ap.add_argument("--out", default=str(DEFAULT_OUT))
args = ap.parse_args()
entities = scan_tables() + scan_schemas() + scan_tscn_buttons() + scan_gui_input_classes() + scan_screens()
_rows, corpus = assertion_corpus()
categories: dict[str, dict] = {}
for ent in entities:
cat = categories.setdefault(ent["category"], {"total": 0, "matched": 0, "gaps": []})
cat["total"] += 1
if ent["id"] and ent["id"] in corpus:
cat["matched"] += 1
else:
cat["gaps"].append({"id": ent["id"], "id_field": ent.get("id_field", "")})
payload = {
"meta": {
"generated_at": datetime.datetime.now().isoformat(timespec="seconds"),
"entities_total": len(entities),
"corpus_rows": len(_rows),
"honest_boundary": "grep 关联有假阳假阴——文本没带 id 的假缺口人工核销;交互命中归探针真输入登记",
},
"categories": [
{"category": k, "total": v["total"], "matched": v["matched"], "gaps": v["gaps"]}
for k, v in sorted(categories.items())
],
}
out = Path(args.out)
out.parent.mkdir(parents=True, exist_ok=True)
js = "/* 本体对账清单——coverage_enum.py 随跑重生成,人工核销走豁免清单(coverage_exemptions.js),不落本文件。 */\n"
js += "window.__QA_COVERAGE__ = window.__QA_COVERAGE__ || {};\n"
js += "window.__QA_COVERAGE__.ontology = %s;\n" % json.dumps(payload, ensure_ascii=False, indent=2)
out.write_text(js, encoding="utf-8", newline="\n")
gap_total = sum(len(c["gaps"]) for c in payload["categories"])
print("[ontology] 本体实体 %d / 断言文料行 %d / 缺口 %d(%d 类)→ %s" % (
len(entities), len(_rows), gap_total, len(payload["categories"]), out))
for c in payload["categories"]:
print(" %s:%d 实体 / %d 缺口" % (c["category"], c["total"], len(c["gaps"])))
return 0
if __name__ == "__main__":
raise SystemExit(main())
+11
View File
@@ -0,0 +1,11 @@
/* 覆盖豁免清单——人工维护(S13 基线人工修订同款纪律;面板与工具只读本文件,禁代码生成)。
* 用途:登记「确认不需要真输入覆盖/不需要断言关联」的条目,B 面板对在册条目亮豁免灯、不亮缺口。
* 形态:abilities=能力清单行「能力」列原文(精确匹配);controls=控件登记键或其中缀(包含匹配);
* ontology=本体实体 id(精确匹配)。每条必须带 reason——无理由的豁免条目面板标红示警。 */
window.__QA_COVERAGE__ = window.__QA_COVERAGE__ || {};
window.__QA_COVERAGE__.exemptions = {
"meta": {"note": "人工维护;改后刷新面板即生效,无生成步骤"},
"abilities": [],
"controls": [],
"ontology": []
};
+184
View File
@@ -0,0 +1,184 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
r"""coverage_receipts.py — 六源回执明细化(B 覆盖总览面板·执行层)
各闸跑批把当场日志落 `.gates/*.log`;本工具在跑批末尾被闸壳调用一次,把六层日志解析成
「名×结果×计数」明细,按闸各写一个 JSONP .js 到 `.gates/coverage/`,供
tools/coverage_report.html 经 <script src> 直载。
六层与日志源(层键=面板消费键,层名=呈现名):
smoke 冒烟测试 smoke_boot.log 摘要「N 项检查,M 项失败」
unit 单元与集成测试 unit.log 每套件「[run_all] PASS/FAIL res://…(exit=N)」
system 系统测试 system_test.log 摘要「N 项检查,M 项失败」
probe 场景探针 probe.log 每探针段「======== 名(段)========」+该段 PASS/FAIL 行
render 渲染闸 render.log golden 清单与对比结果
static 静态扫描 static_scan.log 每个 S 系条目「[static_scan] S# ok/FAIL(…)」
判据口径:
- 明细粒度=「名×结果×计数」,不记逐条断言文本(契约 §4 成本口径);
- 日志缺失=该层 {"result": "not_run"}——面板亮灰灯,不冒充绿;
- 本工具失败不翻转闸判定(观测件非判定件),但必须打红字——静默丢数据等于断言蒸发。
用法: python tools/coverage_receipts.py [--stamp-dir <.gates 路径>]
产出: <stamp-dir>/coverage/gate_<层键>.js × 6
"""
from __future__ import annotations
import argparse
import datetime
import json
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
DEFAULT_STAMP = ROOT / "game" / "tests" / ".gates"
SUMMARY_RE = re.compile(r"摘要[::]\s*\[?[^\]]*\]?\s*摘要[::]\s*(\d+)\s*项检查,(\d+)\s*项失败")
SUITE_RE = re.compile(r"\[run_all\] (PASS|FAIL) (res://\S+?\.gd)(?:\s*\(exit=(-?\d+)\))?")
PROBE_SECTION_RE = re.compile(r"^======== (\S+)(?:\s+(.+?))? ========")
PROBE_RESULT_RE = re.compile(r"^\[[^\]]+\] (PASS|FAIL)")
GOLDEN_RE = re.compile(r"\[render_gate\] \[golden\] (PASS|FAIL)((\d+) 屏黄金图")
GOLDEN_CROP_RE = re.compile(r"\[render_gate\] \[golden_crop\] (\S+?) —")
STATIC_RE = re.compile(r"^\[static_scan\] (S\d+(?:/S\d+)*)\s+(ok|FAIL)", re.M)
STATIC_GDSTYLE_RE = re.compile(r"^\[gdstyle_gate\] (ok|FAIL)", re.M)
def read(stamp: Path, name: str) -> str:
p = stamp / name
if not p.is_file():
return ""
return p.read_text(encoding="utf-8", errors="replace")
def parse_summary_layer(log: str) -> dict:
if not log:
return {"result": "not_run"}
m = SUMMARY_RE.search(log)
verdict = "绿" if "[run_smoke] PASS" in log or "[run_system_test] PASS" in log else (
"红" if ("FAIL" in log or "摘要" in log and m and int(m.group(2)) > 0) else "灰")
if m:
return {"result": verdict, "checks": int(m.group(1)), "fails": int(m.group(2))}
return {"result": verdict}
def parse_unit(log: str) -> dict:
if not log:
return {"result": "not_run"}
items = [{"name": m.group(2), "result": m.group(1), "exit": int(m.group(3) or 0)}
for m in SUITE_RE.finditer(log)]
red = [i for i in items if i["result"] == "FAIL"]
verdict = "绿" if "[run_all] all green" in log and not red else ("红" if red or "FAIL" in log else "灰")
return {"result": verdict, "suites_total": len(items), "items": items}
def parse_probe(log: str) -> dict:
if not log:
return {"result": "not_run"}
items: list[dict] = []
cur: dict | None = None
for line in log.splitlines():
sm = PROBE_SECTION_RE.match(line.strip())
if sm:
seg = (sm.group(2) or "").strip()
cur = {"name": sm.group(1), "segment": seg, "result": "灰"}
items.append(cur)
continue
rm = PROBE_RESULT_RE.match(line.strip())
if rm and cur is not None and cur["result"] == "灰":
cur["result"] = rm.group(1)
judged = [i for i in items if i["result"] != "灰"]
verdict = "红" if (any(i["result"] == "FAIL" for i in judged) or "[probe_gate] 全绿" not in log) else "绿"
return {"result": verdict, "probes_total": len(judged), "items": items}
def parse_render(log: str) -> dict:
if not log:
return {"result": "not_run"}
items: list[dict] = []
gm = GOLDEN_RE.search(log)
if gm:
items.append({"name": "golden 逐像素对比", "result": gm.group(1), "screens": int(gm.group(2))})
screens: list[str] = []
for m in GOLDEN_CROP_RE.finditer(log):
if m.group(1) not in screens:
screens.append(m.group(1))
if screens:
items.append({"name": "golden 逐屏裁剪", "result": "PASS", "screens": screens})
verdict = "红" if "FAIL" in log or "[render_gate] 全绿" not in log else "绿"
return {"result": verdict, "items": items}
def parse_static(log: str) -> dict:
if not log:
return {"result": "not_run"}
items = [{"name": m.group(1), "result": m.group(2)} for m in STATIC_RE.finditer(log)]
gm = STATIC_GDSTYLE_RE.search(log)
if gm:
items.insert(0, {"name": "gdstyle", "result": gm.group(1)})
red = [i for i in items if i["result"] != "ok"]
verdict = "红" if red or "FAIL" in log else "绿"
return {"result": verdict, "items_total": len(items), "items": items}
LAYERS = [
("smoke", "冒烟测试", "smoke_boot.log", parse_summary_layer),
("unit", "单元与集成测试", "unit.log", parse_unit),
("system", "系统测试", "system_test.log", parse_summary_layer),
("probe", "场景探针", "probe.log", parse_probe),
("render", "渲染闸", "render.log", parse_render),
("static", "静态扫描", "static_scan.log", parse_static),
]
def main() -> int:
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
ap = argparse.ArgumentParser(description="六源回执明细化(B 面板·执行层)")
ap.add_argument("--stamp-dir", default=str(DEFAULT_STAMP))
args = ap.parse_args()
stamp = Path(args.stamp_dir)
out_dir = stamp / "coverage"
out_dir.mkdir(parents=True, exist_ok=True)
failed = False
wrote = 0
for key, label, log_name, fn in LAYERS:
try:
detail = fn(read(stamp, log_name))
detail["meta"] = {
"layer": key,
"label": label,
"source_log": log_name,
"generated_at": datetime.datetime.now().isoformat(timespec="seconds"),
}
payload = {"receipt": detail}
js = "/* 闸回执明细化——coverage_receipts.py 随每次跑批覆写,人工修订无效。 */\n"
js += "window.__QA_COVERAGE__ = window.__QA_COVERAGE__ || {};\n"
js += "window.__QA_COVERAGE__.gate_receipts = window.__QA_COVERAGE__.gate_receipts || {};\n"
js += "window.__QA_COVERAGE__.gate_receipts[%s] = %s;\n" % (
json.dumps(key), json.dumps(payload, ensure_ascii=False, indent=2))
(out_dir / ("gate_%s.js" % key)).write_text(js, encoding="utf-8", newline="\n")
wrote += 1
print("[receipts] %s(%s)=%s" % (label, log_name, detail.get("result", "?")))
except Exception as exc: # noqa: BLE001 —— 观测件逐层容错,红字可见不静默
failed = True
print("[receipts] 红:%s 层解析失败(%s):%r" % (label, log_name, exc))
if wrote == 0:
print("[receipts] 红:六层明细一件未落盘——禁静默")
return 1
# 探针登记文件清单(B 面板 file:// 无法列目录——按现存 probe_hits_*.js 落 manifest 供 <script src>)
probe_files = sorted(p.name for p in out_dir.glob("probe_hits_*.js"))
manifest = {
"meta": {"generated_at": datetime.datetime.now().isoformat(timespec="seconds")},
"probes": probe_files,
}
mjs = "/* 探针登记清单——coverage_receipts.py 随跑批按现存文件覆写,勿手编。 */\n"
mjs += "window.__QA_COVERAGE__ = window.__QA_COVERAGE__ || {};\n"
mjs += "window.__QA_COVERAGE__.probe_manifest = %s;\n" % json.dumps(manifest, ensure_ascii=False)
(out_dir / "probe_manifest.js").write_text(mjs, encoding="utf-8", newline="\n")
print("[receipts] 探针登记清单 %d 件 → probe_manifest.js" % len(probe_files))
return 1 if failed else 0
if __name__ == "__main__":
raise SystemExit(main())
+226
View File
@@ -0,0 +1,226 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>QA 覆盖总览面板——Dungeon Sweeper</title>
<style>
:root { --ok:#3a7d44; --bad:#a33; --off:#9aa; --na:#d5d5d0; --ex:#b8860b; --ink:#2c2a26; --paper:#f4f1ea; }
body { font-family: "Microsoft YaHei", sans-serif; margin: 0; background: var(--paper); color: var(--ink); }
header { padding: 14px 20px 10px; border-bottom: 3px solid var(--ink); background: #fff; }
h1 { font-size: 19px; margin: 0 0 4px; }
h2 { font-size: 16px; margin: 22px 0 6px; border-left: 5px solid var(--ink); padding-left: 8px; }
.note { font-size: 12px; color: #555; margin: 3px 0; }
.warn { color: var(--bad); font-weight: bold; }
main { padding: 12px 20px 40px; }
table { border-collapse: collapse; width: 100%; background: #fff; font-size: 12.5px; }
th, td { border: 1px solid #cfc9bd; padding: 4px 7px; text-align: left; vertical-align: top; }
th { background: #ebe6da; }
.lamp { display: inline-block; width: 13px; height: 13px; border-radius: 50%; margin-right: 3px; vertical-align: -2px; }
.ok { background: var(--ok); } .bad { background: var(--bad); }
.off { background: var(--off); } .na { background: var(--na); } .ex { background: #fff; border: 3px double var(--ex); }
.chip { display: inline-block; padding: 1px 8px; border-radius: 9px; color: #fff; font-size: 12px; }
details { margin: 4px 0 10px; } summary { cursor: pointer; font-size: 13px; }
.gaplist { font-size: 12px; color: #444; line-height: 1.7; }
.seg { margin-bottom: 18px; }
code { background: #eceade; padding: 0 3px; }
</style>
</head>
<body>
<header>
<h1>QA 覆盖总览面板 <span style="font-weight:normal;font-size:13px">(义务 × 执行合并视图——只读)</span></h1>
<div class="note">状态灯:绿=该层最近跑批过 红=跑批挂 灰=该层无承载/未跑 双圈=豁免(<code>tools/coverage_exemptions.js</code> 人工维护)。</div>
<div class="note" id="staleness"></div>
<div class="note"><b>完备性边界</b>:控件级视图只对「被探针/C 打开过的屏」负责;未打开过的屏由动线断言+C 探索兜底。
本体对账为文本 grep 关联——有假阳假阴(断言测了实体但文本没带 id=假缺口,人工核销;文本带 id 但断言弱=假覆盖),缺口须人工复核后再立事项。</div>
<div class="note">数据源:随最近一次闸批落盘于 <code>game/tests/.gates/coverage/</code>;刷新本页即读最新。</div>
</header>
<main>
<h2>一、六层闸况</h2>
<div id="gateSummary"></div>
<h2>二、能力清单 × 六层状态灯(按旅程段)</h2>
<div class="note">灯的判据:用例文件落哪层就钉哪层的回执(单元=逐套件 PASS/FAIL;探针/系统/冒烟/渲染/静态=层判据);该层无对应承载=灰。</div>
<div id="capabilityMatrix"></div>
<h2>三、控件级交互覆盖下钻(探针真输入登记)</h2>
<div class="note">交互源=探针真输入命中登记(push_input 管线命中);「存在但零命中」=控件缺口——hitbox 错位/遮挡/驱动空洞在此暴露。展示源=渲染闸 golden 逐屏登记。</div>
<div id="controlDrill"></div>
<h2>四、本体对账(游戏内容 ↔ 测试验证)</h2>
<div class="note">缺口=本体存在、断言文料零关联;若该 id 出现在探针命中登记则降级为「探针承载」。首见批次=本工具生成时间,逐批对比看新面孔。</div>
<div id="ontologyGaps"></div>
</main>
<script src="./coverage_exemptions.js"></script>
<script src="../game/tests/.gates/coverage/capability_list.js"></script>
<script src="../game/tests/.gates/coverage/gate_smoke.js"></script>
<script src="../game/tests/.gates/coverage/gate_unit.js"></script>
<script src="../game/tests/.gates/coverage/gate_system.js"></script>
<script src="../game/tests/.gates/coverage/gate_probe.js"></script>
<script src="../game/tests/.gates/coverage/gate_render.js"></script>
<script src="../game/tests/.gates/coverage/gate_static.js"></script>
<script src="../game/tests/.gates/coverage/probe_manifest.js"></script>
<script src="../game/tests/.gates/coverage/ontology_gaps.js"></script>
<script>
/* 探针登记文件由 manifest 同步补挂(document.write 在解析期阻塞加载,渲染段前数据已就位) */
(function () {
const probes = ((window.__QA_COVERAGE__ || {}).probe_manifest || {}).probes || [];
for (const p of probes) {
document.write('<script src="../game/tests/.gates/coverage/' + encodeURIComponent(p) + '"><\/script>');
}
})();
</script>
<script>
"use strict";
const C = window.__QA_COVERAGE__ || {};
function esc(s) { return String(s == null ? "" : s).replace(/[&<>"]/g, c => ({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"}[c])); }
function lamp(cls, title) { return `<span class="lamp ${cls}" title="${esc(title)}"></span>`; }
function chip(txt, cls) { return `<span class="chip" style="background:${cls}">${esc(txt)}</span>`; }
const VCOLOR = {"绿":"var(--ok)","红":"var(--bad)","灰":"var(--off)","not_run":"var(--off)"};
// ── 数据新鲜度(陈旧可见——防静默旧数据) ──
(function () {
const stamps = [];
const cl = (C.capability_list || {}).meta; if (cl) stamps.push(`义务层 ${cl.generated_at}`);
const rc = C.gate_receipts || {};
for (const k in rc) { const m = (rc[k].receipt || {}).meta; if (m) stamps.push(`${m.label} ${m.generated_at}`); }
const on = (C.ontology || {}).meta; if (on) stamps.push(`本体 ${on.generated_at}`);
const pm = (C.probe_manifest || {}).meta; if (pm) stamps.push(`探针登记 ${pm.generated_at}`);
document.getElementById("staleness").innerHTML = "数据时间:" + stamps.map(esc).join(" · ");
})();
// ── 一、六层闸况 ──
(function () {
const rc = C.gate_receipts || {};
const ORDER = ["smoke", "unit", "system", "probe", "render", "static"];
const LABEL = {smoke:"冒烟测试", unit:"单元与集成测试", system:"系统测试", probe:"场景探针", render:"渲染闸", static:"静态扫描"};
let html = "<table><tr><th>层</th><th>判定</th><th>明细</th></tr>";
for (const k of ORDER) {
const r = (rc[k] || {}).receipt;
if (!r) { html += `<tr><td>${LABEL[k]}</td><td>${chip("缺数据", "var(--off)")}</td><td>—</td></tr>`; continue; }
const v = r.result === "not_run" ? "未跑" : r.result;
const color = VCOLOR[r.result] || "var(--off)";
let detail = `共 ${r.items ? r.items.length : 0} 项`;
if (r.checks != null) detail = `${r.checks} 项检查 / ${r.fails} 失败`;
if (r.suites_total != null) detail = `${r.suites_total} 套件`;
if (r.probes_total != null) detail = `${r.probes_total} 探针`;
html += `<tr><td>${LABEL[k]}</td><td>${chip(v, color)}</td><td>${esc(detail)} <span class="note">源=${esc(r.meta.source_log)} @ ${esc(r.meta.generated_at)}</span></td></tr>`;
}
html += "</table><details><summary>单元与集成逐套件明细</summary><div class='gaplist'>";
const unit = ((rc.unit || {}).receipt || {}).items || [];
for (const it of unit) {
html += `${lamp(it.result === "PASS" ? "ok" : "bad", it.result)} ${esc(it.name.replace("res://tests/", ""))}${it.exit ? "(exit=" + esc(it.exit) + ")" : ""}<br>`;
}
html += "</div></details><details><summary>场景探针逐段明细</summary><div class='gaplist'>";
const pr = ((rc.probe || {}).receipt || {}).items || [];
for (const it of pr) {
html += `${lamp(it.result === "PASS" ? "ok" : (it.result === "FAIL" ? "bad" : "off"), it.result)} ${esc(it.name)}${it.segment ? "(" + esc(it.segment) + ")" : ""}<br>`;
}
html += "</div></details><details><summary>静态扫描逐条明细</summary><div class='gaplist'>";
const st = ((rc.static || {}).receipt || {}).items || [];
for (const it of st) {
html += `${lamp(it.result === "ok" ? "ok" : "bad", it.result)} ${esc(it.name)}<br>`;
}
html += "</div></details>";
document.getElementById("gateSummary").innerHTML = html;
})();
// ── 二、能力清单 × 六层灯 ──
(function () {
const cl = C.capability_list;
if (!cl || !cl.segments) { document.getElementById("capabilityMatrix").innerHTML = "<p class='warn'>义务层缺数据——先跑 tools/extract_capability_list.py</p>"; return; }
const rc = C.gate_receipts || {};
const ex = ((C.exemptions || {}).abilities || []).map(s => String(s));
function layerOf(caseFile) {
if (/system_test/.test(caseFile)) return "system";
if (/probe\//.test(caseFile) || /_probe\.gd/.test(caseFile)) return "probe";
if (/golden|render/.test(caseFile)) return "render";
return "unit";
}
function rowLight(row) {
if (ex.includes(row.ability)) return lamp("ex", "豁免(coverage_exemptions.js 在册)");
if (!row.cases || !row.cases.length) return lamp("off", "无代表用例指针");
let worst = null; const seenLayers = new Set();
for (const c of row.cases) {
const layer = layerOf(c);
seenLayers.add(layer);
const r = (rc[layer] || {}).receipt;
if (!r || r.result === "not_run") { worst = worst || "na"; continue; }
if (r.result === "红") return lamp("bad", `${layer} 层跑批挂`);
if (layer === "unit") {
const items = r.items || [];
const hit = items.filter(it => c.indexOf(it.name.replace("res://tests/", "")) >= 0 || it.name.indexOf(c.split(":")[0].replace(".gd", "")) >= 0);
if (!hit.length) { worst = worst || "off"; continue; }
if (hit.some(it => it.result !== "PASS")) return lamp("bad", "代表套件红:" + c);
worst = worst || "ok";
} else {
worst = worst || (r.result === "绿" ? "ok" : "off");
}
}
if (worst === "ok") return lamp("ok", "承载层绿");
if (worst === "na") return lamp("na", "承载层本批未跑");
return lamp("off", "该层无对应承载(灰)");
}
let html = "";
for (const seg of cl.segments) {
if (!seg.rows.length) continue;
html += `<div class="seg"><h3 style="font-size:14px;margin:8px 0 4px">${esc(seg.segment)}(${seg.rows.length} 行)</h3>`;
html += "<table><tr><th style='width:32%'>能力</th><th>系统</th><th style='width:14%'>灯</th><th style='width:26%'>代表用例</th></tr>";
for (const row of seg.rows) {
html += `<tr><td>${esc(row.ability)}</td><td>${esc(row.system)}</td><td>${rowLight(row)}</td><td>${row.cases.map(esc).join("<br>")}</td></tr>`;
}
html += "</table></div>";
}
document.getElementById("capabilityMatrix").innerHTML = html;
})();
// ── 三、控件级下钻 ──
(function () {
const pm = C.probe_manifest || {probes: []};
const ex = ((C.exemptions || {}).controls || []).map(s => String(s));
const rd = ((C.gate_receipts || {}).render || {}).receipt || {};
const renderScreens = [];
for (const it of (rd.items || [])) if (it.screens instanceof Array) renderScreens.push(...it.screens);
let html = "";
if (renderScreens.length) {
html += `<p class="note"><b>渲染展示登记(golden 逐屏)</b>:${renderScreens.map(esc).join("、")}</p>`;
}
for (const pf of pm.probes) {
const name = pf.replace(/^probe_hits_/, "").replace(/\.js$/, "");
// 同名重复 <script> 无法动态注入——manifest 落盘时同步内联一份数据入口由面板经 window 取
const data = (window.__QA_COVERAGE__.probe_real_input || {})[name];
if (!data) { html += `<p class="warn">${esc(name)}:登记文件未加载(检查 script 标签与 manifest)</p>`; continue; }
const hits = data.hits || {};
const seen = data.seen || [];
const gaps = seen.filter(k => !(k in hits) && !ex.some(e => k.indexOf(e) >= 0));
html += `<details open><summary><b>${esc(name)}</b>:命中 ${Object.keys(hits).length} 控件 / 存在 ${seen.length} 控件 / 零命中缺口 ${gaps.length}(@ ${esc(data.meta.generated_at)})</summary><div class='gaplist'>`;
for (const g of gaps) html += `${lamp("off", "存在但零真输入命中")} ${esc(g)}<br>`;
html += "<b>已命中:</b><br>";
for (const k of Object.keys(hits).sort()) html += `${lamp("ok", "真输入命中 " + hits[k] + " 次")} ${esc(k)}×${hits[k]}<br>`;
html += "</div></details>";
}
document.getElementById("controlDrill").innerHTML = html || "<p class='warn'>探针登记缺数据——先跑探针与 tools/coverage_receipts.py</p>";
})();
// ── 四、本体对账 ──
(function () {
const on = C.ontology;
if (!on || !on.categories) { document.getElementById("ontologyGaps").innerHTML = "<p class='warn'>本体数据缺——先跑 tools/coverage_enum.py</p>"; return; }
const ex = ((C.exemptions || {}).ontology || []).map(s => String(s));
const hitTexts = [];
const pri = (C.probe_real_input || {});
for (const k in pri) for (const hk of Object.keys(pri[k].hits || {})) hitTexts.push(hk);
let html = `<p class="note">实体 ${esc(on.meta.entities_total)} · 断言文料 ${esc(on.meta.corpus_rows)} 行 · 首见批次 ${esc(on.meta.generated_at)}</p>`;
for (const cat of on.categories) {
const shown = cat.gaps.filter(g => !ex.includes(g.id));
const probed = shown.filter(g => hitTexts.some(h => h.indexOf(g.id) >= 0));
const pure = shown.filter(g => !probed.includes(g));
html += `<details><summary><b>${esc(cat.category)}</b>:${cat.total} 实体 / 关联 ${cat.matched} / 缺口 ${shown.length}(其中探针承载 ${probed.length})</summary><div class='gaplist'>`;
for (const g of probed) html += `${lamp("ok", "探针真输入命中承载")} ${esc(g.id)} <span class="note">(断言文料零关联——交互覆盖由探针承载)</span><br>`;
for (const g of pure) html += `${lamp("off", "验证零关联缺口——人工复核后再立事项")} ${esc(g.id)}<br>`;
html += "</div></details>";
}
document.getElementById("ontologyGaps").innerHTML = html;
})();
</script>
</body>
</html>
+129
View File
@@ -0,0 +1,129 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
r"""extract_capability_list.py — 能力清单义务层提取器(B 覆盖总览面板·义务层)
把 game/tests/README.md「能力清单」的四列表(能力|系统|真实入口|代表用例)按旅程段
解析成机器可读 JSONP .js,供 tools/coverage_report.html 经 <script src> 直载
(file:// 下 fetch 本地 json 被 CORS 拦——JSONP 是 B 面板全数据源的统一形态)。
用法:
python tools/extract_capability_list.py [--readme <路径>] [--out <路径>]
判据口径:
- 只认表头恰为「能力|系统|真实入口|代表用例」的四列表(覆盖域表等五列表不取);
- 旅程段标题形态=「**旅程段N:名称**」,段名进每行的 segment 字段;
- 代表用例列按「;」切分成用例指针数组(`文件:函数` 形态逐字保真);
- 本工具是义务层的静态投影,产出随 README 变更重跑即得——面板只读本产物,禁在面板里改义务。
产出(JSONP):
window.__QA_COVERAGE__.capability_list = {
"meta": {"generated_at", "source", "rows_total", "segments_total"},
"segments": [{"segment": 名称, "rows": [{"ability", "system", "entry", "cases": []}]}],
}
"""
from __future__ import annotations
import argparse
import datetime
import json
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
DEFAULT_README = ROOT / "game" / "tests" / "README.md"
DEFAULT_OUT = ROOT / "game" / "tests" / ".gates" / "coverage" / "capability_list.js"
SEGMENT_RE = re.compile(r"^\*\*旅程段[一二三四五六七八九十]+[::](.+?)\*\*\s*$")
def split_row(line: str) -> list[str]:
body = line.strip()
if body.startswith("|"):
body = body[1:]
if body.endswith("|"):
body = body[:-1]
return [c.strip() for c in body.split("|")]
def parse(readme: Path) -> dict:
lines = readme.read_text(encoding="utf-8").splitlines()
segments: list[dict] = []
cur_seg: str | None = None
cur_rows: list[dict] | None = None
in_capability_table = False
for line in lines:
seg = SEGMENT_RE.match(line.strip())
if seg:
if cur_seg is not None:
segments.append({"segment": cur_seg, "rows": cur_rows or []})
cur_seg = seg.group(1).strip()
cur_rows = []
in_capability_table = False
continue
if cur_seg is None:
continue
if not line.strip().startswith("|"):
in_capability_table = False
continue
cells = split_row(line)
if all(not set(c) - {"-", " ", ":"} for c in cells) and cells:
continue # 表头分隔行 |---|---|
if (len(cells) == 4 and cells[0] == "能力" and cells[1] == "系统"
and cells[2] == "真实入口" and cells[3].startswith("代表用例")):
in_capability_table = True
continue
if in_capability_table and len(cells) == 4:
ability, system, entry, cases = cells
cur_rows.append({
"ability": ability.strip("`"),
"system": system,
"entry": entry,
"cases": [c.strip().strip("`") for c in cases.split(";") if c.strip()],
})
if cur_seg is not None:
segments.append({"segment": cur_seg, "rows": cur_rows or []})
rows_total = sum(len(s["rows"]) for s in segments)
return {
"meta": {
"generated_at": datetime.datetime.now().isoformat(timespec="seconds"),
"source": str(readme),
"rows_total": rows_total,
"segments_total": len(segments),
},
"segments": segments,
}
def dump_js(payload: dict, out: Path) -> None:
out.parent.mkdir(parents=True, exist_ok=True)
js = "/* 能力清单义务层投影——由 extract_capability_list.py 生成,随 README 变更重跑;人工修订无效(义务源=README 能力清单)。 */\n"
js += "window.__QA_COVERAGE__ = window.__QA_COVERAGE__ || {};\n"
js += "window.__QA_COVERAGE__.capability_list = %s;\n" % json.dumps(payload, ensure_ascii=False, indent=2)
out.write_text(js, encoding="utf-8", newline="\n")
def main() -> int:
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
ap = argparse.ArgumentParser(description="能力清单义务层提取(B 面板·义务层)")
ap.add_argument("--readme", default=str(DEFAULT_README))
ap.add_argument("--out", default=str(DEFAULT_OUT))
args = ap.parse_args()
readme = Path(args.readme)
if not readme.is_file():
print("[capability] README 不存在:%s" % readme)
return 1
payload = parse(readme)
dump_js(payload, Path(args.out))
print("[capability] 义务层投影已落盘 %s(旅程段 %d / 能力行 %d)" % (
args.out, payload["meta"]["segments_total"], payload["meta"]["rows_total"]))
if payload["meta"]["rows_total"] == 0:
print("[capability] 红:能力行数为 0——表头或旅程段标题形态不匹配,禁当空义务面用")
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())
+38 -17
View File
@@ -5,7 +5,8 @@
# 微光色值与呈现参数 = game/presentation/views/ui_kit.gd TG_GLOW_*/TYPE_GLOW_*(v2 冻结);
# 合成公式 = game/presentation/views/cell_type_glow.gdshader(blend_mix 距离场柔光);
# 带域/格心取样 = game/probe/type_glow_shot_probe.gd(de<格边×0.125 / 内缩 20%);
# 硬指标门 = art-bible §4.1(L≥0.30 线性域 / 饱和≤0.55 / 避 300°–345°);
# 准入门值(亮度地板/饱和上限/禁入色相带/近色容差)单点在本件 FLOOR_L·CEIL_SAT·
# BANNED_HUE·NEAR_EPS;art-bible §4.1 只述「未开格底与微光存在相对亮度关系」并指路本件。
# 既有色板比对池 = game/tests/golden/palette_baseline/ 两张条带并集(palette_check.py 同源)。
# 方案 §2.3 机检 C 口径订正:模型 0.49/0.51 是反解近似(ui_kit.gd 头注「模型域与实测的差
# = shader 距离场权重非扁平混合」),比较对象改为 shader 精确合成模型(逐像素复刻),判据线
@@ -43,12 +44,13 @@ CELL = 45 # D 区实比格边
BAND = CELL * BAND_FRAC # 5.625
CORE_PAD = int(CELL * 0.20) # 格心内缩(探针 CORE_PAD_FRAC=0.20)
# ── 三关铺底代表值 = 色表墙顶受光家族最亮槽 H2(tools/recolor_terrain_tiles.py PALETTES)──
H2_VALUES = {
"dungeon": (181, 155, 119),
"underdark": (186, 193, 210),
"deep_underdark": (150, 158, 186),
}
# ── 铺底准入门值单点(文档侧只述「未开格底与微光存在相对亮度关系」,数值归本件)──
FLOOR_L = 0.30 # 未开格底相对亮度地板(线性域)
CEIL_SAT = 0.55 # 饱和度上限
BANNED_HUE = (300.0, 345.0) # 禁入色相带(Boss 紫赤独占)
NEAR_EPS = 8 # 近色容差(三通道差全 <此值 即近色)
B_MIN_CH_DIFF = 8 # 机检 B|格底-微光可辨底线(三通道至少一通道 ≥此值)
C_MODEL_TOL = 2 # 机检 C|样图与合成模型容差(±此值 /255)
BOARD_TYPES = ["dungeon", "underdark", "deep_underdark"]
BASE_VARIANTS = [ # (底型名,瓦片件名后缀)——素砖 + 两种装饰变体(高墙瓦片词表在册件)
("素砖", "terrain_wall_center"),
@@ -139,6 +141,23 @@ def load_tiles():
return tiles
def lit_slots(tiles):
"""铺底代表值 = 该关三块底型瓦片像素里线性亮度最大的实测色(顶面受光档)。
零表值:不引用任何色表常量,素材换色后读数自动跟随。"""
slots = {}
for bt in BOARD_TYPES:
best = None
for vn, _ in BASE_VARIANTS:
px = tiles[(bt, vn)]
for y in range(CELL):
for x in range(CELL):
c = px[x, y]
if best is None or rel_lum(c) > rel_lum(best):
best = c
slots[bt] = best
return slots
# ── 带域/格心取样(探针口径)──
def band_pixels():
"""带域像素集(整数格坐标 de < 格边×0.125,type_glow_shot_probe 口径)。"""
@@ -187,21 +206,22 @@ def load_baseline_palette():
def gate_d(tiles):
baseline = load_baseline_palette()
lit = lit_slots(tiles)
rows = []
all_pass = True
for bt in BOARD_TYPES:
rgb = H2_VALUES[bt]
rgb = lit[bt]
h, s = hsv(rgb)
lin = rel_lum(rgb)
l_ok = lin >= 0.30
s_ok = s <= 0.55
hue_ok = not (300.0 <= h <= 345.0)
l_ok = lin >= FLOOR_L
s_ok = s <= CEIL_SAT
hue_ok = not (BANNED_HUE[0] <= h <= BANNED_HUE[1])
# 近色门:三值=在役素材色(已在基线池内),palette_check 双门②只判新增色——报读数不判红
near = [b for b in baseline if all(abs(rgb[i] - b[i]) < 8 for i in range(3))]
near = [b for b in baseline if all(abs(rgb[i] - b[i]) < NEAR_EPS for i in range(3))]
ok = l_ok and s_ok and hue_ok
all_pass = all_pass and ok
rows.append((bt, rgb, lin, s, h, len(near), ok))
check("D-" + bt, "铺底代表值门(L/饱和/禁入带;近色只报读数)", ok,
check("D-" + bt, "铺底代表值门(底型瓦片实测顶面受光档;亮度/饱和/禁入带,近色只报读数)", ok,
"RGB=%s L=%.3f 饱和=%.3f 色相=%.1f° 近色对=%d(在役素材色,非新增)%s%s%s"
% (rgb, lin, s, h, len(near),
"" if l_ok else " [L红]", "" if s_ok else " [饱和红]",
@@ -300,6 +320,7 @@ def run_checks(img, cell_pngs, tiles, blocks, gray_top, main_top, main_bot):
table2 = [] # 90 行组间表
band_means = {}
fail_b, fail_c = [], []
lit = lit_slots(tiles)
for bt, y0 in blocks:
for vn, _ in BASE_VARIANTS:
@@ -321,13 +342,13 @@ def run_checks(img, cell_pngs, tiles, blocks, gray_top, main_top, main_bot):
model = model_band_mean(px, glow)
diff_model = max(abs(band_mean[i] - model[i]) for i in range(3))
ch_diff = [abs(band_mean[i] - core_mean[i]) for i in range(3)]
b_ok = max(ch_diff) >= 8
c_ok = diff_model <= 2
b_ok = max(ch_diff) >= B_MIN_CH_DIFF
c_ok = diff_model <= C_MODEL_TOL
if not b_ok:
fail_b.append("%s/%s/%s" % (bt, vn, gname))
if not c_ok:
fail_c.append("%s/%s/%s" % (bt, vn, gname))
hue_gap = abs(hsv(H2_VALUES[bt])[0] - hsv(glow)[0])
hue_gap = abs(hsv(lit[bt])[0] - hsv(glow)[0])
hue_gap = min(hue_gap, 360 - hue_gap)
table1.append((bt, vn, gname, band_mean, core_mean, model,
diff_model, ch_diff, hue_gap, b_ok and c_ok))
@@ -396,7 +417,7 @@ def write_report(table1, table2, d_rows):
]
for cid, name, ok, detail in CHECKS:
lines.append("| %s | %s | %s:%s |" % (cid, "PASS" if ok else "FAIL", name, detail))
lines += ["", "## S1 机检 D:铺底代表值(H2 槽)五门", "",
lines += ["", "## S1 机检 D:铺底代表值(底型瓦片实测顶面受光档)五门", "",
"| 关 | RGB | L(线性域) | 饱和 | 色相 | 近色对 | 判定 |", "|---|---|---|---|---|---|---|"]
for bt, rgb, lin, s, h, near, ok in d_rows:
lines.append("| %s | %s | %.3f | %.3f | %.1f° | %d | %s |" % (bt, rgb, lin, s, h, near, "过" if ok else "红"))
+2
View File
@@ -682,5 +682,7 @@ RC=$?
echo ""
echo "════════════════════════════════════════════"
printf ' 回执:%s\n 基线:%s\n commit:%s\n' "$RECEIPT" "$BASELINE" "$(git -C "$REPO" rev-parse --short HEAD 2>/dev/null || echo '?')"
# B 覆盖总览面板·执行层六源明细化(观测件非判定件——解析失败打红字不翻闸判,coverage_receipts.py 头注)
"$PY" "$REPO/tools/coverage_receipts.py" --stamp-dir "$STAMP_DIR" || true
echo "════════════════════════════════════════════"
exit $RC
+16 -13
View File
@@ -2,14 +2,6 @@
本清单是流水线的唯一登记处:以后才做的事在此登记一条,随流水线段推进改状态,做完整条删除(条目记进度,不是各段的开工前置);各系统文档的「待办」节只留一行指路存根,不再自建条目。条目格式(标题=登记元数据含可选优先级,正文=实况/缺口/改动面/状态四属性)与状态机以 [doc-conventions §12.1](design/engineering/doc-conventions.md) 为权威。
#### 三关色相 × 五组微光互辨抽测——三关图类型色板终值冻结的前置抽测(2026-10-28 登记,来源=会话提及)
**实况**:三关图类型色板(`tile_closed` 与 `wall_ambient` 两槽)终值冻结前须先跑「三关色相 × 五组类型微光」互辨抽测(格底与类型微光同在未开格面,邻近色相有吞没风险);美术设计段已闭合——取样面 54 格(3 关 × 5 组 × 3 底型+9 基准格)、判据=目检四条+机检四条(阈值全取在役值,色相通道不设机检阈值)、产物=并列样图+逐对读数表两兼。执行步骤与三件定值全载抽测方案过程稿 reports/提案-三关色相互辨抽测方案-2026-10-29.md;抽测挂点=[美术圣经](design/art/art-bible.md) v3.22 §6.3 终值取值程序第一条(终值冻结闸=§1.4 格子样张重跑)。执行前提已核:三关瓦片三套在本机 `game/assets/art/board/` 齐备,可开跑。
**改动面**:文档+美术素材+工具代码
**状态**:美术设计已闭合+待执行
#### 数值标定窗口总挂账——原九条数值类条目合并(2026-10-28 登记,来源=会话提及)
**实况**:九条数值类挂账合并一条(削弱档位终值/迭代 10 奖励重构联动/调参点评登记/三方向解锁清单与定价/GP 经济换算失衡/天赋树欠账余两件/商店刷新两参数/基线通关率越带杠杆扫描/成长系数节奏),逐条明细与统一执行形态(窗口内一次 MC 批量覆盖全冒险期望+奖励期望)全载 reports/汇总-数值标定窗口挂账-2026-10-28.md。共同裁定:未设计不启动,重启条件=用户发令开数值标定窗口。MC 档位硬闸见 sim-data/README.md「MC 采集档位表」。
@@ -245,19 +237,19 @@
#### 测试面真输入化分期与存量断言逐条精锚(2026-11-05 登记,来源=批次移交)
**实况**:证据=test-conventions §8.3「判据出处行锚」「真实入口判定口径」两条文(本批落);链式锚机检已在闸(capability_audit 链断 0,80 套件全承接)。剩余面=①营地七物件段等 ~40 处 emit 站点仍为接线验证(大门/进档链已于本批真输入化,能力行 :158 残险在案)②存量 996 条断言的判据出处逐条精锚(链式锚已保可核,精锚提升出处粒度)。
**实况**:证据=test-conventions §8.3「判据出处行锚」「真实入口判定口径」两条文+§2「UI 驱动断言须走真输入管线」条文(批1补,qa-harness 分支 2cbd2ca9);链式锚机检已在闸(capability_audit 链断 0,80 套件全承接)。已承接=两探针 52 处 emit 站点(51 转真输入管线+1 置灰穿透标〔接线断言〕)。剩余面=①视图层测试方法面(press_candidate/press_continue/press_card/press_confirm/press_refresh_for_probe 等探针消费口,非 emit 站点)②存量 996 条断言的判据出处逐条精锚(链式锚已保可核,精锚提升出处粒度)。
**改动面**:游戏代码
**状态**:待执行
#### QA 覆盖总览面板与自动试玩扩展——自动发现测试验证缺漏(2026-11-05 登记,来源=会话提及)
#### QA harness 三批缺陷与覆盖遗漏收口(2026-11-06 登记,来源=批次移交,优先级=P0)
**实况**:方案全文=`D:\code\qa-harness-discussion\QA-Harness讨论结果-中间报告.md`(三批定案,全部三关试点);证据=真实入口缺口在册:能力行「营地七物件命中区点选与分界面可达」残险行 game/tests/README.md:158(emit 直发标「接线验证」、真输入承载待建)+test-conventions §8.3 真实入口判定口径条款(2026-11-05 收窄)。①探针存量 emit 真输入化(~40 处站点,承接上条)②B 覆盖总览面板+本体枚举器(六源回执明细化+能力清单提取+HTML 双击即开+游戏内容↔测试验证对账)③C 自动试玩扩展(auto_player 扩全动线全功能,异常上报含机器自填归因)。裁决在案勿重提:A 执行覆盖矩阵不做、D 缺口闸不做、变异测试禁入、归因登记不立独立制度。
**实况**:证据=reports/QA-harness三批-缺陷与覆盖遗漏报告-2026-11-06.md(八类逐条带文件行号与登记处指针:产品缺陷 2=A1 开局被开始层盖住/A2 终局关闭路径引擎报错噪声;测试覆盖遗漏 6=B1 停手钮零真输入/B2 视图方法面/B3 精锚/B4 本体缺口 212 条/B5 控件零命中缺口/B6 判据收紧轮);来源批次=合并 d3c85390(三批读数在案)。处置分流:A1 随营地交互修复批合回即消;A2/B1/B2 走需求性工程写卡通道;B3/B6 归衔接条目与复核轮;B4/B5 人工复核后分流(真缺口立事项/假缺口豁免清单核销)。
**改动面**:游戏代码+工具代码+文档
**改动面**:游戏代码
**状态**:待执行
**状态**:待写卡
#### 闸机超时守卫缺陷修复——gate_run_timeout 标志文件恒真(2026-11-06 登记,来源=批次移交)
@@ -274,3 +266,14 @@
**改动面**:游戏代码
**状态**:待执行
#### 美术圣经去数值化——定值改由机检件与数据表承载(2026-11-06 登记,来源=会话提及)
**实况**:本条立文档分层规矩——美术圣经与 GDD 同级属 Meta 层,只写概念与方向(可写「两者存在相对亮度关系」),不写具体数值约束;阈值与准入判定归执行件,执行件登记面=[art-pipeline-tools](design/art/art-pipeline-tools.md)(三关色板互辨抽测件本批已入册)。
- 相关文档:纪律出处=[doc-conventions §5 禁实现内容行](design/engineering/doc-conventions.md);同款先例=[08-Alpha版本标准](design/gdd/08-Alpha版本标准.md) 二节「面值一律不在此抄录,以数据表为准」
**缺口**:还差一次清点与归属设计讨论——圣经全部定值逐条定落点(机检件/代码常量/数据表/golden 基线)、定文档侧指针句式、核下游反向引用面(ux 文档、需求卡片、探针注引圣经定值之处)。
**改动面**:文档+工具代码
**状态**:待文档设计