diff --git a/design/engineering/test-conventions.md b/design/engineering/test-conventions.md index 9b1bd547..07fbbb98 100644 --- a/design/engineering/test-conventions.md +++ b/design/engineering/test-conventions.md @@ -97,12 +97,18 @@ - **反向**:diff 里每个行为改动至少一行;没有对应行的改动,必须写明「非行为改动」或「由第 X 行覆盖」。 - **测试侧**:新增或改写的每条断言至少挂一行;挂不上的孤儿断言删掉或补挂到需求点。 +**判据出处行锚(2026-11-05 立,自 2026-11-05 起对文件名日期不早于该日的需求卡片生效;旧卡不回头改,被编入迭代开工前由写卡段按本粒度补挂)**:对照表「需求点」列每行必须带**判据出处行锚**——该行验收判据所依据的现行文档句子,格式「`<文档相对路径>:<行号>`」。现行文档=`design/` 全域现行文档(含 `ux/` 交互规格与 art-bible 的判据行);已退役的需求卡片不是现行文档,不得充当判据出处。作用=断言预期值从规格句抄出,不从实现反推:实现行为与规格句冲突时断言红;规格缺席的行写不出锚,即停卡退回写卡段(那是需求澄清缺口,不是测试缺口)。机检=`tools/pipeline_gate.py` 判据 C5(锚格式与指向存在性,按卡文件名日期分流)+`tools/capability_audit.py` 链式锚检查(断言→套件→能力行→系统文档,见 §8.9)。 + **验收档申报(2026-11-04 立,自 2026-11-05 起对文件名日期不早于该日的需求卡片生效;旧卡不回头改)**:每张需求卡片在头部信息区(与设计事实源声明同区)申报一个验收档,三档:高/中/低。判据=失败代价单维——这条改动做错了,后果随存档或结算传播的(存档写读与迁移、RNG 流、战斗结算公式与乘区、经济入账、数值表结构校验)=高;后果玩家可见但不损持久状态的(常规玩法逻辑、门控、交互逻辑、呈现数据面)=中;后果即看即知且无状态残留的(文案、纯呈现样式、布局微调)=低。一张卡触多档按最高档申报;申报由写卡段落笔,用户批卡(U0)时确认或改判。档位只调节 §8.7 审计深度——闸机与 CI 的域划分、选闸算法、全量强制档一概不因档位变化,真实入口、判据绑定面、禁削弱等验收闭合判据全部照旧。未申报档位的卡一律按高档全量审计。机检=`tools/pipeline_gate.py` 判据 C4:卡内须含「验收档:高/中/低」申报行。 **能力清单维护锚(2026-10-29 用户裁改版定稿)**:`game/tests/README.md`「能力清单」是**实现指导+阅读导航+义务声明**——只宣布「要验证什么」,不记「是否已验证/是否跑过」;落地账归现行验收闭合链(改动—验收对照表逐条挂断言+合回前独立验收审计),本清单不承载验证状态。表为四列:能力|系统|真实入口|代表用例(`文件:函数`)。**维护归写卡段,当场入表**:写需求卡片时,本批新增能力当场写入能力行——真实入口取自设计文档已定的入口,代表用例=卡里定死的计划测试(`文件:函数`);受影响存量行(入口/用例名变更)同批更新;能力退役删行。**实现照单落名**:测试函数名以清单为唯一依据,名实不符=卡片没写清=停卡退回。**收尾核对**:批末收尾只核对表上名字与实际落地一致(改名则回表),不刷任何批次字段;实现批禁写该文件的现行红线不变(process-conventions §3 第 11 条),核对义务归收尾提交。三层模型见 §8.9。改动—验收对照表纪律不变:新增或改写的每条断言仍须挂一行(上文「测试侧」);断言改判仍受 §8.4 真实入口口径约束。 +**「真实入口」判定口径(2026-11-05 收窄)**:真实入口只认经引擎输入管线(合成 InputEvent 走 `push_input`)或真实渲染路径(截图/golden)的承载;信号直发(`pressed.emit()` 等合成信号)承载一律标「接线验证」,不得充当能力行的真实入口——接线验证保「信号已接线」,保不了「玩家点击可达」(hitbox/命中面在信号上游,emit 全部绕过)。既有能力行若以 emit 承载充当真实入口,随批触碰时订正;暂未触碰的行,缺口显式登记(残险模式),禁静默豁免。 + **新增 core 件的覆盖登记义务(2026-11-03 架构审查 P1 余项裁)**:拆件或重构产出的新 core 文件不触发 process-conventions §3 第 6 条「新功能必配新测试」(那条只管新增玩法/机制),因此单由本条承接——新 core 件要么被 `game/tests/` 某套件按文件名或 `class_name` 点名,要么在承接它的能力行「代表用例」里申报「经宿主链覆盖(宿主=<件>)」;两者都无=批末收尾核对时补登。本条**不是覆盖率要求**,与 §7「覆盖率不是目标」同轨:登记的是能力义务的归属,不做文件计数——「每个 core 件必须被点名一次」的机器闸已裁否(它逼出为点名而点名的空套件,且与 §7 正面冲突)。落笔面沿用现行分工:申报行由写卡段入能力清单(实现批禁写该文件),核对归批末收尾提交。 +**退役对账义务(2026-11-05 立)**:需求卡片退役(吸收即死)时,其对照表每行的验收意图必须已被承接——承接形态=能力清单行(能力+真实入口+代表用例)或已落地的断言;退役后无承接=该能力的规格判据断链,收尾核对判红。机检=`tools/capability_audit.py`(套件→能力行反向覆盖+断言链核验)。先例教训:营地图批卡片退役后,大门判据(`design/gdd/ux/camp.md:16`)无任何活面承接,探针 emit 断言悬空假绿——规格在、链接断。 + ### 8.4 真实入口断言(验收闭合的主判据) - **唯一口径(2026-10-24/2026-10-28 用户两裁)**:每个需求点与每个行为改动的验收=一条从真实入口进入的断言,可 grep 定位、在当轮真实跑批中执行并通过。**禁变异类测试**(把生产码或数据表改成错值看断言抓不抓得到);**禁撤销类取证**(回退本批改动或该 hunk 看断言红不红)——两者都是人为制造红,属伪造证据,一律禁止。 @@ -127,6 +133,7 @@ 2. 抽查判据绑定面:断言从真实入口进入、钉在被测改动的特有可观测提交面上,且当轮真实跑批执行过; 3. 检查测试削弱:删除断言、放宽阈值、缩小扫描面、换下层直调,逐处要卡片申报或用户批准。 4. 核对内部字段直查的受控例外:直查行是否已在对照表注明理由(该行为无公开可观测面可钉)。 +5. 核对判据出处:抽查断言的预期值能否从其判据出处行锚(§8.3)的规格句独立写出——预期值只在实现里有的(同义反复,oracle 镜像实现),判红。 **审计深度分级(2026-11-04 立)**:审计按卡内申报的验收档(§8.3)逐卡定深度;梯度只落在昂贵的判断件上,机械且堵造假向量的两件(削弱检查、例外核对)三档全保留不抽。中/低档省略的测试复跑以远端 CI 读数为执行证据——`tools/ci_check.sh <合并提交>` 的现查输出(commit+run 号+conclusion)是机器产物,run 号在远端永久可复核,审计报告照抄该行即可;照抄前先核对读数必须是「绿」——「未验」(零触发批跑闸步全跳过)与「不可判」不作证据,那种批的回补办法是等一次含代码面的合回或用户自发全量回归(2026-11-04 用户裁:回执等待件退役,证据由落盘文件改为现查读数;「不采信实现者自述已全绿」的口径不变): diff --git a/game/probe/probe_ui_util.gd b/game/probe/probe_ui_util.gd index f885dfb2..52952792 100644 --- a/game/probe/probe_ui_util.gd +++ b/game/probe/probe_ui_util.gd @@ -118,6 +118,29 @@ static func isolate_save(main, tag: String, mark_first_done: bool = false, with_ main.settings_store = SettingsStoreScript.new("user://%s_settings.cfg" % tag) return iso +# ---- 真输入点击承载(test-conventions §8.3「真实入口」判定口径)---- +# 合成鼠标事件经 Viewport.push_input 进引擎输入管线(hitbox/命中测试参与),press+release 成对 +#(BaseButton pressed 信号走 release 沿——ui_input_probe :717-733 / screen_flow_probe :484-499 同式)。 +# 信号直发(pressed.emit())只许标「接线验证」,不得充当真实入口断言的驱动。 +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: + var down := InputEventMouseButton.new() + down.position = pos + down.global_position = pos + down.button_index = MOUSE_BUTTON_LEFT + down.pressed = true + main.get_viewport().push_input(down, true) + var up := InputEventMouseButton.new() + up.position = pos + up.global_position = pos + up.button_index = MOUSE_BUTTON_LEFT + up.pressed = false + main.get_viewport().push_input(up, true) + + # 营地经进档开层(卡② mn9 新路径——campbtn 已撤;槽模板注入隔离命名空间,防碰真实槽 # user://save_slot_N.json——真实档红线)。创建=激活=暂行开营地(卡③ Q3 乙落点); # 驱动后写盘目标=隔离槽文件(持久化断言读 user://_slot_1.json)。返回 false=新档钮缺失(驱动中断)。 @@ -130,8 +153,9 @@ static func open_camp_via_savesel(main, tag: String) -> bool: if FileAccess.file_exists(sp): DirAccess.remove_absolute(sp) main._open_save_select() + await main.get_tree().process_frame # 排版帧——真点击命中的是控件 rect,开层同帧容器未排(emit 无此依赖) var new_btn: Button = main.ui.modals.menu.save_select_overlay._new_btns.get(1) if new_btn == null: return false - new_btn.pressed.emit() # 真实按钮信号路径 → _on_save_select_new → create_slot+open_camp + push_click(main, new_btn) # 真输入 → _on_save_select_new → create_slot+open_camp return true diff --git a/game/probe/screen_flow_probe.gd b/game/probe/screen_flow_probe.gd index 8c538e35..30b65976 100644 --- a/game/probe/screen_flow_probe.gd +++ b/game/probe/screen_flow_probe.gd @@ -110,7 +110,7 @@ func _step_boot_menu() -> void: # ---- ② 菜单 → 营地(真实按钮信号路径)---- func _step_menu_to_camp() -> void: # 营地经进档开层(卡② mn9 新路径——campbtn 已撤;槽模板注入隔离命名空间) - if not ProbeUiUtil.open_camp_via_savesel(main, "smoke_screen_flow_camp"): + if not await ProbeUiUtil.open_camp_via_savesel(main, "smoke_screen_flow_camp"): _problems.append("②菜单→营地:选档层新档钮缺失(新路径驱动中断)") return await _frames() @@ -141,7 +141,7 @@ func _step_save_select_open_back() -> void: if esb == null: _problems.append("[savesel] mn3 进入存档钮缺失(卡② 交付物)") return - esb.pressed.emit() # 真实按钮信号路径 → _open_save_select(卡② mn3 点击链——卡③ 直调中间态就此闭环) + ProbeUiUtil.push_click(main, esb) # 真输入(push_input 管线)→ _open_save_select(卡② mn3 点击链——真实入口承载) await get_tree().process_frame var seals := _screen_seals() if not main.ui.modals.menu.save_select_overlay.visible or not seals_check_single(seals): @@ -238,7 +238,7 @@ func _step_end_back_to_menu() -> void: # ---- ③' 营地 Esc 浮窗+HUD(卡⑤——真实键盘合成路径 push_input;四态路由+三出口+语境分支+余额单口)---- func _step_camp_esc_and_hud() -> void: ProbeUiUtil.isolate_save(main, "smoke_screen_flow_esc", false, false, true) - if not ProbeUiUtil.open_camp_via_savesel(main, "smoke_screen_flow_esc"): + if not await ProbeUiUtil.open_camp_via_savesel(main, "smoke_screen_flow_esc"): _problems.append("[esc] 营地入口缺失(选档层新档钮驱动中断)〔ESC批 V1〕") return await _frames() @@ -301,7 +301,7 @@ func _step_camp_esc_and_hud() -> void: # 互斥单开/ESC 优先序/窗内滚动/生涯同壳——行为面(开合路由)零改,只钉窗壳可观测提交面。 func _step_camp_window_shell() -> void: ProbeUiUtil.isolate_save(main, "smoke_screen_flow_winshell", false, false, true) - if not ProbeUiUtil.open_camp_via_savesel(main, "smoke_screen_flow_winshell"): + if not await ProbeUiUtil.open_camp_via_savesel(main, "smoke_screen_flow_winshell"): _problems.append("[winshell] 营地入口缺失(选档层新档钮驱动中断)〔窗口化批 V1〕") return await _frames() diff --git a/game/probe/shot_probe.gd b/game/probe/shot_probe.gd index d7e92577..1981c89e 100644 --- a/game/probe/shot_probe.gd +++ b/game/probe/shot_probe.gd @@ -120,7 +120,7 @@ func _ready() -> void: if main.ui.modals.menu.career_overlay.visible or not main.ui.screens.start_overlay.visible: _problems.append("生涯页关闭后页未隐藏或开始层未恢复") # ② 营地场景图(进档开层新路径——卡② mn9;截图=可点击场景图形态——卡④ 契约 9 V1 验收载体) - if not ProbeUiUtil.open_camp_via_savesel(main, "smoke_shot_probe_camp"): + if not await ProbeUiUtil.open_camp_via_savesel(main, "smoke_shot_probe_camp"): _problems.append("营地入口缺失(选档层新档钮——新路径驱动中断)〔句柄子容器批 V1〕") else: await get_tree().process_frame diff --git a/game/probe/ui_click_probe.gd b/game/probe/ui_click_probe.gd index 637f418a..a1347621 100644 --- a/game/probe/ui_click_probe.gd +++ b/game/probe/ui_click_probe.gd @@ -61,7 +61,7 @@ func _ready() -> void: # swap=true(整链换注——购买写盘经 autoload 落隔离槽文件,与下方持久化核验同源; # 原 swap=false 形态的写盘走 iso 实例旧路径,与槽文件断言不同源——卡② mn9 换代) var iso_c = ProbeUiUtil.isolate_save(main, "smoke_click_probe_camp", false, false, true) - if not ProbeUiUtil.open_camp_via_savesel(main, "smoke_click_probe_camp"): + if not await ProbeUiUtil.open_camp_via_savesel(main, "smoke_click_probe_camp"): _problems.append("营地入口缺失(选档层新档钮——mn9 新路径驱动中断)〔句柄子容器批 V1〕") else: await get_tree().process_frame @@ -302,7 +302,7 @@ func _ready() -> void: if settings_btn == null: _problems.append("设置入口按钮缺失(开始层 settingsbtn)") else: - settings_btn.pressed.emit() # 真实按钮信号路径 → _open_settings + ProbeUiUtil.push_click(main, settings_btn) # 真输入(push_input 管线)→ _open_settings await get_tree().process_frame if not main.ui.modals.menu.settings_overlay.visible or main.ui.screens.start_overlay.visible: _problems.append("设置面未打开(点击路径)") @@ -685,7 +685,7 @@ func _ready() -> void: main._camp_pres.open_camp() await get_tree().process_frame var bal_gate: int = main._camp_data()["balance"] - main.ui.modals.camp.scene.object_pressed.emit(&"gate") + ProbeUiUtil.push_click_at(main, main.ui.modals.camp.scene.get_hotspot_rect(&"gate").get_center()) await get_tree().process_frame if not main.ui.modals.menu.confirm_dialog.is_open(): _problems.append("大门无断点:确认弹窗未开(点大门应先出确认不直接进冒险)〔营地图批 V3〕") @@ -697,9 +697,9 @@ func _ready() -> void: 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〕") - main.ui.modals.camp.scene.object_pressed.emit(&"gate") + ProbeUiUtil.push_click_at(main, main.ui.modals.camp.scene.get_hotspot_rect(&"gate").get_center()) await get_tree().process_frame - main.ui.modals.menu.confirm_dialog.confirmed.emit() # 「是」→开始新冒险 + ProbeUiUtil.push_click(main, main.ui.modals.menu.confirm_dialog._confirm_btn) # 真点「确认」→开始新冒险 await get_tree().process_frame if not main.run_scope.is_adventure or main.ui.modals.camp.scene.visible: _problems.append("大门确认:冒险未发起或营地场景屏未隐藏〔营地图批 V3〕〔营地图批 B1〕") @@ -712,11 +712,11 @@ func _ready() -> void: }) main._camp_pres.open_camp() await get_tree().process_frame - main.ui.modals.camp.scene.object_pressed.emit(&"gate") + ProbeUiUtil.push_click_at(main, main.ui.modals.camp.scene.get_hotspot_rect(&"gate").get_center()) await get_tree().process_frame if not main.ui.modals.menu.confirm_dialog.is_open() or not main.ui.modals.menu.confirm_dialog._secondary_btn.visible: _problems.append("大门有断点:三出口形态失守(次钮应现形)〔营地图批 V3〕") - main.ui.modals.menu.confirm_dialog.secondary_confirmed.emit() # 「开始新冒险」出口 + ProbeUiUtil.push_click(main, main.ui.modals.menu.confirm_dialog._secondary_btn) # 真点「开始新冒险」出口 await get_tree().process_frame if not main.run_scope.is_adventure or main.ui.modals.camp.scene.visible: _problems.append("大门有断点次确认:冒险未发起或营地未隐藏〔营地图批 V3〕") diff --git a/game/probe/ui_probe.gd b/game/probe/ui_probe.gd index 2e6295ca..d738ce83 100644 --- a/game/probe/ui_probe.gd +++ b/game/probe/ui_probe.gd @@ -24,7 +24,7 @@ func _ready() -> void: # 隔离存档换注 main.meta 保确定性(禁碰真实档 user://save.json——SaveManager 路径注入纪律; # 营地先行段只换 meta 不换 save_manager——冒险段换注零档在前)---- ProbeUiUtil.isolate_save(main, "smoke_ui_probe_camp", false, false, false) - if not ProbeUiUtil.open_camp_via_savesel(main, "smoke_ui_probe_camp"): + if not await ProbeUiUtil.open_camp_via_savesel(main, "smoke_ui_probe_camp"): problems.append("营地入口缺失(选档层新档钮——mn9 新路径驱动中断)〔句柄子容器批 V1〕") else: await get_tree().process_frame diff --git a/game/probe/ui_struct_probe.gd b/game/probe/ui_struct_probe.gd index 0a77e69c..d4b67b0e 100644 --- a/game/probe/ui_struct_probe.gd +++ b/game/probe/ui_struct_probe.gd @@ -38,7 +38,7 @@ func _ready() -> void: main._on_settings_closed() _settle() - if not ProbeUiUtil.open_camp_via_savesel(main, "smoke_ui_struct_probe_menu"): + if not await ProbeUiUtil.open_camp_via_savesel(main, "smoke_ui_struct_probe_menu"): problems.append("[camp] 营地入口缺失(选档层新档钮——mn9 新路径驱动中断)〔句柄子容器批 V1〕") else: _settle() diff --git a/game/tests/README.md b/game/tests/README.md index 28addd2d..eadbd3f2 100644 --- a/game/tests/README.md +++ b/game/tests/README.md @@ -70,13 +70,13 @@ tests/ | 能力 | 系统 | 真实入口 | 代表用例(文件:函数) | |---|---|---|---| -| 冒险生命周期:开局受理/三关序列/败北线 | 冒险结构 | `AdventureRunner.start_adventure()`/`continue_to_next_level()` | `adventure_feats_test.gd:_smoke_adventure_api`;`adventure_feats_test.gd:_smoke_adventure` | +| 冒险生命周期:开局受理/三关序列/败北线 | 冒险结构 | `AdventureRunner.start_adventure()`/`continue_to_next_level()` | `adventure_feats_test.gd:_smoke_adventure_api`;`adventure_feats_test.gd:_smoke_adventure`;`hero_ledger_test.gd:_init` | | 专长(奇物)授予与凭证保底 | 奇物 | `AdventureRunner.present_node_curio()`/`feats_held()` × 效果引擎兑付 | `adventure_feats_test.gd:_smoke_feats`;`feat_vouchers_test.gd:_init` | | 节点图生成合法性与同种子复现 | 关卡节点图生成 | `NodeMapGenerator.generate()`/`validate_output()` | `node_map_generator_test.gd:_test_output_ok`;`node_map_generator_test.gd:_test_reproducible`;`node_map_generator_test.gd:_test_exit_structure` | | 节点图生长递增与无解 fail-fast | 关卡节点图生成 | `NodeMapGenerator.generate()`/`is_fatal()` | `node_map_growth_test.gd:_test_growth`;`node_map_growth_test.gd:_test_fatal_subprocess` | | 节点图参数读表与域单点(三参数驱动产物/生成器侧可行性保留) | 关卡节点图生成 | `NodeMapGenerator.generate()`/`LevelGenParams.validate()` | `node_map_generator_test.gd:_test_gen_params_table_driven`;`table_structure_guard_test.gd:_test_level_gen_new_fields` | | 起关/选路/断点重生成同图(集成) | 关卡节点地图×关卡节点图生成×冒险结构 | `LevelRunner.start_level()`/`start_level_external()`/`pick_next_node()`;`AdventureRunner.resume_adventure()` | `node_map_determinism_test.gd:_smoke_generated_map_determinism` | -| 信息迷雾类型可见性 | 关卡节点地图 | `LevelRunner.type_visible_ids()` | `level_map_type_vis_test.gd:_test_layer_distance`;`level_map_type_vis_test.gd:_test_boss_exempt`;`level_map_type_vis_test.gd:_test_resume_rebuild` | +| 信息迷雾类型可见性 | 关卡节点地图 | `LevelRunner.type_visible_ids()` | `level_map_type_vis_test.gd:_test_layer_distance`;`level_map_type_vis_test.gd:_test_boss_exempt`;`level_map_type_vis_test.gd:_test_resume_rebuild`;`level_map_info_fog_test.gd:_initialize` | | 关卡表消费侧校验与关卡推进 | 关卡节点地图×数据配置 | `LevelTable.validate_graph()`;`LevelRunner.start_level()` | `level_table_validate_test.gd:_test_positive`;`level_table_validate_test.gd:_test_negative`;`level_shop_economy_test.gd:_smoke_level` | **旅程段三:战斗结算** @@ -86,8 +86,8 @@ tests/ | 伤害公式与掷点乘区 | 战斗结算 | `DamageMath.pool_damage()`/`roll_discount_percent()`/`rolled_component()` | `damage_math_test.gd:_test_pool_damage`;`damage_math_test.gd:_test_roll_discount_percent`;`roll_multiplier_test.gd:_smoke_armor_roll` | | 伤害单据编排与回放 | 战斗结算 | `DamageController.submit()`/`open_ticket()`/`flush()` | `damage_controller_test.gd:_run_recipe_sequence`;`damage_controller_test.gd:_test_five_recipes_and_replay` | | 效果发射端口单点 | 战斗结算 | `BattleOrchestrator.begin_battle()`(真实装配链端口接线+合成行闭面守卫经 `DamageController.flush_submit()` 承伤路径) | `effect_port_test.gd:_smoke_effect_port_wiring`;`effect_port_test.gd:_test_business_classes_engine_free`;`effect_port_test.gd:_test_biz_fact_face_closed` | -| 轮内流与结算五段全序 | 战斗结算 | `BattleOrchestrator.begin_battle()` | `round_flow_skill_pool_test.gd:_smoke_full_battle`;`round_flow_skill_pool_test.gd:_smoke_settlement_segments` | -| 技能池数据面与回放独立 | 敌人系统 | `BattleOrchestrator.begin_battle()`(技能池抽号/校验) | `round_flow_skill_pool_test.gd:_smoke_skill_pool_data`;`round_flow_skill_pool_test.gd:_smoke_skill_pool_replay_independence` | +| 轮内流与结算五段全序 | 战斗结算 | `BattleOrchestrator.begin_battle()` | `round_flow_skill_pool_test.gd:_smoke_full_battle`;`round_flow_skill_pool_test.gd:_smoke_settlement_segments`;`settle_segments_test.gd:_init`;`settlement_wiring_snapshot_test.gd:_init`;`encapsulation_sweep_test.gd:_init` | +| 技能池数据面与回放独立 | 敌人系统 | `BattleOrchestrator.begin_battle()`(技能池抽号/校验) | `round_flow_skill_pool_test.gd:_smoke_skill_pool_data`;`round_flow_skill_pool_test.gd:_smoke_skill_pool_replay_independence`;`trigger_cond_test.gd:_init`;`pool_case_d_test.gd:_init` | | Boss 半血解锁技门控 | 敌人系统 | `BattleOrchestrator.begin_battle()`(Boss 行) | `skill_unlock_test.gd:_test_gate_predicate`;`skill_unlock_test.gd:_test_latch_irreversible` | | 封印与极性路由/不可逆 | 地图战斗资源×战斗结算 | 效果引擎 × `BattleOrchestrator.begin_battle()` | `seal_polarity_test.gd:_smoke_seal_ap_exhausted`;`seal_polarity_test.gd:_smoke_seal_irreversible_quadrants`;`seal_polarity_test.gd:_smoke_polarity_routing` | | 变异布置门控 | 地形变异 | `BattleOrchestrator.begin_battle()`(变异布置段) | `variant_placement_test.gd:_smoke_variant_placement`;`variant_placement_test.gd:_smoke_global_variant`;`variant_placement_test.gd:_test_pack_min_from_table` | @@ -103,7 +103,7 @@ tests/ | 元进度入账与锻造两轴 | 元进度×锻造工坊 | `MetaStateStore.apply_adventure_summary()`/`MetaStateStore.spend_keystone()`/`MetaStateStore.set_forge_route_level()` | `meta_progression_forge_test.gd:_meta_pricing_and_buy`;`meta_progression_forge_test.gd:_smoke_forge_axes` | | 锻造定价与购买闭环(全路线价格梯/多级购买扣款/拒购零副作用) | 锻造工坊 | `ForgeTable.level_price()`/`MetaProgression.forge_buy_level()` | `forge_table_test.gd:_test_price_ladder`;`meta_progression_forge_test.gd:_meta_pricing_and_buy`;`meta_progression_forge_test.gd:_test_forge_buy_reject` | | 锻造与相邻通道隔离(祝福件购买锻造零变化/默认轴与钥匙解耦/甲型切换双轴可感) | 锻造工坊×元进度 | `MetaProgression.forge_select()`/`hero_type_axes()`/商店购买口 | `meta_progression_forge_test.gd:_test_blessing_no_forge_leak`;`meta_progression_forge_test.gd:_test_default_axes_pre_key`;`meta_progression_forge_test.gd:_test_armor_switch_dual_axes` | -| 天赋树购买与六键合成 | 天赋树 | `MetaProgression.tree_buy()`/`MetaStateStore.tree_state()`/`MetaStateStore.set_tree_level()` | `meta_progression_forge_test.gd:_smoke_tree`;`meta_progression_forge_test.gd:_smoke_tree_rules`;`tree_rules_v11_test.gd:_init` | +| 天赋树购买与六键合成 | 天赋树 | `MetaProgression.tree_buy()`/`MetaStateStore.tree_state()`/`MetaStateStore.set_tree_level()` | `meta_progression_forge_test.gd:_smoke_tree`;`meta_progression_forge_test.gd:_smoke_tree_rules`;`tree_rules_v11_test.gd:_init`;`tree_requires_level_test.gd:_init` | | 规则键参数读表(六键参数字典校验与激活侧透传) | 天赋树 | `MetaTreeTable.validate()`/`EffectEngine.rebuild()`/`MetaProgression.tree_rules()` | `tree_rules_v11_test.gd:_test_rule_key_params` | | 秘宝结构冻结与 12 件效果 | 秘宝 | `TreasureItems.add_item()`/`query_inventory()`/`validate_use()` | `treasure_items_test.gd:_init`;`treasure_effects_test.gd:_init` | @@ -124,7 +124,7 @@ tests/ | 停手转存储备与轮内刷新(上限/AP 不重置/刷新图零清图奖励) | 行动经济 | `BattleOrchestrator.player_request_refresh()`/`MinesweeperRules.settle_batch()` | `round_flow_skill_pool_test.gd:_smoke_active_refresh`;`seal_polarity_test.gd:_smoke_refresh_provenance_guard`;`refresh_feats_test.gd:test_no_feats_baseline` | | 换图系刷新链路与效果词表接线 | 行动经济×战斗结算×奇物 | `MinesweeperRules.settle_batch()`/`BattleOrchestrator.player_request_refresh()` | `effect_words_t1_test.gd:_init`;`forge_table_test.gd:_test_effect_kinds_closed`;`refresh_feats_test.gd:test_board_refresh_fact_only_on_success` | | 换图基座:挖格产 RAP 与刷新折扣 | 行动经济×奇物 | `MinesweeperRules.settle_batch()`/`BattleOrchestrator.player_request_refresh()` | `refresh_feats_test.gd:test_reserve_on_accepted_dig`;`refresh_feats_test.gd:test_refresh_discount_gate_and_charge`;`refresh_feats_test.gd:test_double_base_stack` | -| 换图输出:刷新群伤与蓄力攻击 | 战斗结算×奇物 | `BattleOrchestrator.player_request_refresh()`→轮末 `MinesweeperRules.settle_batch(stop)` | `refresh_feats_test.gd:test_blast_defers_to_combat_segment`;`refresh_feats_test.gd:test_charge_layers_consumed_same_round` | +| 换图输出:刷新群伤与蓄力攻击 | 战斗结算×奇物 | `BattleOrchestrator.player_request_refresh()`→轮末 `MinesweeperRules.settle_batch(stop)` | `refresh_feats_test.gd:test_blast_defers_to_combat_segment`;`refresh_feats_test.gd:test_charge_layers_consumed_same_round`;`run_time_fatal_test.gd:_init` | | 换图生存:全局固定减伤与轮末恢复 | 战斗结算×奇物 | `BattleOrchestrator.player_request_refresh()`→敌方攻击/危险格/错误封印/地形真实受伤入口 | `refresh_feats_test.gd:test_all_hero_damage_paths_emit_once`;`refresh_feats_test.gd:test_stone_mitigation_all_damage`;`refresh_feats_test.gd:test_rest_rotation_cap_and_clear` | | 换图资源:刷新固定 GP 收益 | 战斗结算×奇物 | `BattleOrchestrator.player_request_refresh()` | `refresh_feats_test.gd:test_mine_profit_gp` | | 下轮行动点修正排队与兑现 | 行动经济 | `ActionEconomy.pending_ap_delta()`(排队经 `ActionEconomy.queue_ap_delta()`,写入方为效果引擎) | `seal_polarity_test.gd:_smoke_negative_dictionary`;`feat_vouchers_test.gd:_init` | @@ -155,8 +155,8 @@ tests/ | 战斗叙述与终局文案(数值带入/胜负互异) | HUD框架 | `BattleNarrator.battle_end_text()`/`BattleNarrator.boss_name()` | `battle_narrator_test.gd:_test_numeric_lines_carry_values`;`battle_narrator_test.gd:_test_end_text_differs_by_result`;`battle_narrator_test.gd:_test_boss_name_uses_boss_rowset` | | 玩家面文案与危险格格面字形 | HUD框架 | `BoardView.rebuild()`/`StatusSlotRow.set_effects()` | `player_copy_sweep_test.gd:_sweep_board_glyph`;`player_copy_sweep_test.gd:_sweep_legend`;`player_copy_sweep_test.gd:_sweep_hero_panel` | | 营地场景图十层铺底与门洞留空呈现 | 营地场景视图 | 进入存档→选档→进档→营地场景屏(`CampSceneView.setup_layers()` 驱动);单元测试载入 `camp_scene_layout.tres` | `shot_probe.gd:_ready`(camp 截图段);`camp_scene_layout_test.gd:_test_layout_hotspot_geometry` | -| 营地七物件命中区点选与分界面可达 | 营地场景视图 | 七物件命中区 → `CampSceneView._gui_input()`(命中发 `object_pressed` 信号) → 组合根开界面 | `ui_click_probe.gd:_ready`(七物件合成点击段) | -| 营地大门两分支确认与断点续玩接线 | 营地交互流 | 大门命中区 → ConfirmDialog(三出口)→ `main._on_confirm_accepted()`(新冒险确认)/`main._resume_from_menu()`(断点续玩) | `ui_click_probe.gd:_ready`(大门两分支段/断点确认段 :500-528) | +| 营地七物件命中区点选与分界面可达 | 营地场景视图 | 七物件命中区 → `CampSceneView._gui_input()`(命中发 `object_pressed` 信号) → 组合根开界面;**现役承载=emit 信号直发,标「接线验证」——真输入承载待建(残险登记,2026-11-05 真实入口口径收窄)** | `ui_click_probe.gd:_ready`(七物件合成点击段) | +| 营地大门两分支确认与断点续玩接线 | 营地交互流 | 大门命中区 → 合成鼠标事件经 `push_input` 真点(`ProbeUiUtil.push_click_at`)→ ConfirmDialog 真钮(三出口)→ `main._on_confirm_accepted()`(新冒险确认)/`main._resume_from_menu()`(断点续玩) | `ui_click_probe.gd:_ready`(大门两分支段/断点确认段——真输入 push_click 驱动,2026-11-05 真输入化) | | 四柜台拆解后五界面入口改挂与写口零回归 | 营地五界面 | 五界面意图信号 → `CampShopPresenter` → `MetaProgression.tree_buy()` 写口 | `forge_workbench_test.gd:_run`(承载类同迁断言十二项);`ui_click_probe.gd:_ready`(写口链段) | | 营地篝火回主菜单与笔记本改挂动线 | 营地交互流 | 笔记本命中区 → `CareerOverlay.open()`;篝火命中区 → ConfirmDialog 二次确认 → 主菜单 | `ui_struct_probe.gd:_ready`(career 段);`screen_flow_probe.gd:_ready`(篝火/开合段) | | 营地图鉴占位挂起态与反参照结构纪律 | 营地场景视图 | 布告板命中区 → `CampSceneView._gui_input()`(object_pressed 信号) → 图鉴占位视图;营地根态场景树审计 | `ui_struct_probe.gd:_ready`(占位禁词/禁行段);`ui_struct_probe.gd:_ready`(结构禁词段) | @@ -174,13 +174,13 @@ tests/ | 进度坐标查行:精确命中/向上取整/封顶/警告去重 | 数据配置 | `BoardGenTable.resolve_row()`/`ConfigDbResolver.resolve_enemy_rowset()` | `config_db_test.gd:_test_progress_resolution`;`config_db_resolver_test.gd:_test_band_hit`;`config_db_resolver_test.gd:_test_top_band_clamp` | | 行集寻址、行序保序与难度乘数 clamp | 数据配置×难度曲线与Boss | `ConfigDbResolver.resolve_enemy_rowset()`/`ConfigDbResolver.resolve_difficulty_modifiers()` | `config_db_resolver_test.gd:_test_rowset_order_preserved`;`config_db_resolver_test.gd:_test_row_kind_routing`;`config_db_resolver_test.gd:_test_difficulty_clamp` | | 运行期越域致命与进程非 0 退出 | 数据配置 | `BoardGenTable.resolve_row()`/`ConfigDbResolver.resolve_enemy_rowset()` | `config_db_fatal_oob_test.gd:_trigger_oob`;`config_db_resolver_test.gd:_test_request_oob` | -| 加载期违规表拒绝:跨表悬空/计数器前缀/真视窄域/效果类型出域 | 数据配置×秘宝×奇物 | `ConfigDB.load_tables()` | `reward_dangling_treasure_test.gd:_init`;`counter_prefix_fatal_test.gd:_assert_rejected`;`treasure_target_violation_test.gd:_init`;`effect_kind_retired_test.gd:_assert_rejected` | +| 加载期违规表拒绝:跨表悬空/计数器前缀/真视窄域/效果类型出域 | 数据配置×秘宝×奇物 | `ConfigDB.load_tables()` | `reward_dangling_treasure_test.gd:_init`;`counter_prefix_fatal_test.gd:_assert_rejected`;`treasure_target_violation_test.gd:_init`;`effect_kind_retired_test.gd:_assert_rejected`;`extended_row_violation_test.gd:_init` | | 表结构守卫:域校验/行序键/跨字段互锁/配额锚 | 数据配置×敌人系统×地形变异×冒险结构 | `EnemyTable.validate()`/`VariationTable.variant_pool()`/`AdventureTable.validate()` | `table_structure_guard_test.gd:_smoke_enemy_bands`;`table_structure_guard_test.gd:_smoke_variation_framework`;`table_structure_guard_test.gd:_smoke_adventure_table`;`table_structure_guard_test.gd:_test_feat_cap_required` | | 变异表头域校验(地形变异) | 地形变异 | `VariationTable.validate()` | `table_structure_guard_test.gd:_test_variation_header_domains` | | 难度修饰套乘:空表零行为/套乘生效/越域 clamp | 难度曲线与Boss | `UnitContainer.spawn()`/`ConfigDbResolver.resolve_difficulty_modifiers()` | `table_structure_guard_test.gd:_smoke_difficulty_table`;`config_db_resolver_test.gd:_test_difficulty_clamp` | | Boss 出场门控(每区域恰一 Boss 终点/Boss 节点开战必选 Boss 行集) | 难度曲线与Boss | `NodeMapGenerator.generate()`/`BattleOrchestrator.begin_battle()` | `node_map_generator_test.gd:_test_boss_exit_per_region`;`boss_gate_test.gd:_test_boss_node_spawns_boss_rowset` | | 锻造表 schema 正反与几何价格梯 | 锻造工坊 | `ForgeTable.level_price()`/`ForgeTable.route_rows()` | `forge_table_test.gd:_test_production_rows`;`forge_table_test.gd:_test_price_ladder`;`forge_table_test.gd:_test_negative_cases` | -| 系统间契约词表与受理口径:五配方互异/掷点标签成员性/增益布尔受理 | 战斗结算×数据配置×增益状态 | `DamageController.open_ticket()`/`StatusContainer.apply_effect()` | `system_contracts_test.gd:_test_damage_controller_vocabularies`;`system_contracts_test.gd:_test_rng_tag_closure`;`system_contracts_test.gd:_test_status_boolean_acceptance` | +| 系统间契约词表与受理口径:五配方互异/掷点标签成员性/增益布尔受理 | 战斗结算×数据配置×增益状态 | `DamageController.open_ticket()`/`StatusContainer.apply_effect()` | `system_contracts_test.gd:_test_damage_controller_vocabularies`;`system_contracts_test.gd:_test_rng_tag_closure`;`system_contracts_test.gd:_test_status_boolean_acceptance`;`source_guard_test.gd:_init` | | 同名状态合并策略(叠加/累加/并存)与剩余次数语义 | 战斗循环×增益状态 | `StatusContainer.apply_effect()`(编排宿主 begin_battle 链) | `roll_multiplier_test.gd:_smoke_armor_roll` | | 状态效果结算链(逐值衰减/到期移除/战斗结束全清/满层拒绝/独立并存/首授快照) | 增益状态 | `StatusContainer.apply_effect()`/`decay_round()`/`notify_battle_end()` | `system_contracts_test.gd:_test_status_decay_and_clear`;`system_contracts_test.gd:_test_status_stack_full_reject`;`system_contracts_test.gd:_test_status_independent_branch`;`system_contracts_test.gd:_test_status_snapshot_first_grant` | | 状态实例按个体归属:个体隔离/全体授予逐体发/死亡即清/同体合并照旧 | 增益状态 | `StatusContainer.apply_effect()`(个体键)/`purge_owner()` | `system_contracts_test.gd:_test_status_individual_isolation`;`system_contracts_test.gd:_test_status_grant_all_per_unit`;`system_contracts_test.gd:_test_status_death_clears`;`system_contracts_test.gd:_test_status_merge_per_unit` | @@ -332,7 +332,7 @@ S12 效果引擎无状态化旧件零残留:扫描 game/ 全域 `.gd` 与 `gam ## 本地闸(唯一验收门禁) -本仓库无远端、无 CI——**验收只在本地发生**,跑 `tools/run_all_gates.sh` 是收口唯一入口,按本批改动域选档(`--code`/`--docs`/跨两域或改闸件本身=不带参数全量;代码域 02 按关联档选套件,回执 `scope` 字段记 full|related), +**验收=合回后远端 CI 闸全绿**(2026-11-03 用户裁,取代本地的合回验收;游戏码批=两域全量、02 全套件);`tools/run_all_gates.sh` 仍是全部验证闸的唯一入口与唯一清单,本地按改动域选档跑批(`--code`/`--docs`/跨两域或改闸件本身=不带参数全量;代码域 02 按关联档选套件,回执 `scope` 字段记 full|related)只服务开发期窄测,**不构成验收**, 跑完写回执 `game/tests/.gates/receipt.json`(含 `domain` 与 `scope` 字段),合回 master 由 worktree-guard 按 diff 文件性质推域校验回执(`game/` 内 `.md` 归文档域)。 闸清单与域划分(含选闸算法与套件族映射表)的唯一定义处就是那个脚本,本文档不另列清单(清单散落多份文档正是历史漏跑闸的成因)。 @@ -346,6 +346,19 @@ S12 效果引擎无状态化旧件零残留:扫描 game/ 全域 `.gd` 与 `gam **gdUnit4 维持 deferred**:不引入第三方测试框架,本仓库在既有载体(`--script` 套件+场景运行目标)内解决。 +## 闸门耗时基线(超时对比依据) + +各闸在本机(Windows 基准档)的预期耗时区间与耗时模型。单次跑批实测显著超出区间上限(约 2× 中值)时,先核对环境因素(导入缓存重建、杀毒扫描、显示器休眠)再判性能回归。区间为单机粗测值;闸回执后续若记录实际耗时,以其积累值替换本表。 + +| 闸 | 耗时模型 | 预期区间 | 备注 | +|---|---|---|---| +| 00 冒烟 | O(1)——引擎启动一次+存活检查 | ~10–20 秒 | | +| 01 系统测试 | O(屏壳装配数 × 断言面规模) | ~2–3 分钟 | 断言计数随面浮动(口径见「断言计数双口径」节) | +| 02 单元与集成 | O(套件 manifest 条目数 × 单套件冷启动) | ~3–6 分钟 | 每套件独立子进程;单套件挂死守卫 120 秒 | +| 03 场景探针 | O(探针段数 × 引擎冷启动) | ~3–5 分钟(--full/--click 各计一次) | 耗时大头=每段一次引擎冷启动 | +| 05 渲染 | O(截图段数 × 窗口化冷启动 + golden 比对) | ~8–15 分钟 | 五段串行,需显示器在位 | +| 远端 CI 全量 | 上列各闸串联(两域全量) | ~30–35 分钟 | 2026-11-04 远端 run 实测 ≈33 分钟(单点样本) | + ## 术语(本项目内的唯一语义) 标准四级测试层(2026-10-22 用户裁,权威对照=design/gdd/术语表.md「退役词对照」): diff --git a/tools/capability_audit.py b/tools/capability_audit.py index 8d29555d..64dcb891 100644 --- a/tools/capability_audit.py +++ b/tools/capability_audit.py @@ -185,6 +185,7 @@ def capability_rows(): def check_capability_rows(cap_tables, suites, funcs, cards): total = 0 names = [] + referenced = set() suite_by_base = {} for rel in suites: suite_by_base.setdefault(Path(rel).name, []).append(rel) @@ -207,13 +208,25 @@ def check_capability_rows(cap_tables, suites, funcs, cards): red(README.relative_to(REPO), no, "能力行结构", "「%s」行格数不足或空列(%s)" % (name[:16], "/".join(empty) or "列数")) continue + referenced.update(m.group(1) for m in CASE_RE.finditer(short("代表用例"))) if "条件闸" in short("代表用例"): continue check_cases(no, name, short("代表用例"), suite_by_base, funcs, cards) check_entry(no, name, short("真实入口"), funcs, cards) if total == 0: selfcheck.append("能力表零数据行") - return names + return names, referenced + + +def check_suite_coverage(suites, referenced): + """链式锚反向覆盖(test-conventions §8.3 退役对账):run_all.gd manifest 的每个套件 + 必须被 ≥1 个能力行「代表用例」引用——断言(尾缀挂卡/挂套件)→套件→能力行→系统文档 + 的链条中,套件未接能力行=卡片退役后验收意图断链(营地图批先例)。""" + missing = [rel for rel in suites if Path(rel).name not in referenced] + for rel in sorted(missing): + red(README.relative_to(REPO), 1, "链式锚", + "套件 %s 未被任何能力行「代表用例」引用——断言→套件→能力行→系统文档链条断环" % rel) + return missing def check_cases(no, name, cell, suite_by_base, funcs, cards): @@ -323,7 +336,8 @@ def main(): funcs = product_funcs() if not funcs: selfcheck.append("产品码零 `func`") - names = check_capability_rows(cap_tables, suites, funcs, cards_corpus()) + names, referenced = check_capability_rows(cap_tables, suites, funcs, cards_corpus()) + missing_suites = check_suite_coverage(suites, referenced) check_coverage(cov_tables, names) if selfcheck: @@ -331,9 +345,9 @@ def main(): for s in selfcheck: print(" ! " + s) - print("| ── 能力审计:系统文档 %d 篇 · 能力行 %d · 套件 %d · 覆盖域表 %d 行 · 红项 %d" + print("| ── 能力审计:系统文档 %d 篇 · 能力行 %d · 套件 %d · 覆盖域表 %d 行 · 链断套件 %d · 红项 %d" % (len(system_docs()), len(names), len(suites), - sum(len(r) for _, r in cov_tables), len(reds))) + sum(len(r) for _, r in cov_tables), len(missing_suites), len(reds))) for path, line, check, msg in reds: print(" ✗ [%s] %s:%d %s" % (check, path, line, msg)) if selfcheck: diff --git a/tools/pipeline_gate.py b/tools/pipeline_gate.py index ff76d229..2ea41b98 100644 --- a/tools/pipeline_gate.py +++ b/tools/pipeline_gate.py @@ -63,6 +63,9 @@ CAP_README = Path("game/tests/README.md") # C4 验收档申报(2026-11-04 验收强度分级批,生效 2026-11-05;test-conventions §8.3) GRADE_RULE_DATE = (2026, 11, 5) GRADE_RE = re.compile(r"验收档\s*[::]\s*[高中低]") +# C5 判据出处行锚(2026-11-05 立;test-conventions §8.3:需求点格须带现行文档判据句锚) +ANCHOR_RULE_DATE = (2026, 11, 5) +ANCHOR_RE = re.compile(r"([A-Za-z0-9_\-.\\/]+\.(?:md))\s*:\s*(\d+)") # 待办条目正文限长双判据(2026-11-04 立,doc-conventions §12.1:行数抓清单蔓延、字数抓单行堆砌) ENTRY_MAX_LINES = 12 ENTRY_MAX_CHARS = 700 @@ -498,10 +501,19 @@ def capability_names(root): return names -def check_ledger(text, path, when, issues, root): - """C3 改动—验收对照表结构检查(生效 2026-10-21)。 +def anchor_valid(root, rel, lineno): + """判据出处行锚有效性:文档存在且行号在行数范围内(相对仓库根解析)。""" + p = root / rel.replace("\\", "/") + if not p.is_file(): + return False + return 1 <= lineno <= len(p.read_text(encoding="utf-8-sig").splitlines()) - 只查:对照表节在不在、表头有没有五个必需列、有没有数据行、必填格空不空。 + +def check_ledger(text, path, when, issues, root): + """C3 改动—验收对照表结构检查(生效 2026-10-21);C5 判据出处行锚(生效 2026-11-05)。 + + 只查:对照表节在不在、表头有没有五个必需列、有没有数据行、必填格空不空; + C5=生效日后需求点格须带现行文档行锚且指向存在(test-conventions §8.3)。 不判断测试是否真的覆盖改动——那是独立验收审计的职责,机器闸不猜语义。 """ lines = text.splitlines() @@ -614,6 +626,16 @@ def check_ledger(text, path, when, issues, root): issues.append((str(path), i + 1, "能力行不可核", "既有能力「%s」不在 game/tests/README.md 能力表" % cap_name[:24])) + if when >= ANCHOR_RULE_DATE: + point_cell = cells[idx["需求点"]] if idx.get("需求点", 99) < len(cells) else "" + anchors = ANCHOR_RE.findall(point_cell) + if not anchors: + issues.append((str(path), i + 1, "判据出处行锚缺失", + "「需求点」格须带判据出处行锚 `<现行文档相对路径>:<行号>`" + "(2026-11-05 起,test-conventions §8.3)")) + elif not any(anchor_valid(root, rel, int(no)) for rel, no in anchors): + issues.append((str(path), i + 1, "判据出处行锚不可核", + "锚指向的文档或行号不存在:%s" % point_cell[:40])) if rows == 0: issues.append((str(path), header_line, "验收对照表零行", "表内没有数据行")) diff --git a/待办清单.md b/待办清单.md index 6f4129f0..b069c19d 100644 --- a/待办清单.md +++ b/待办清单.md @@ -286,3 +286,13 @@ **改动面**:游戏代码 **状态**:文档设计已闭合+已写卡+已编迭代 52 + +#### 测试面真输入化分期与存量断言逐条精锚(2026-11-05 登记,来源=批次移交) + +**实况**:证据=test-conventions §8.3「判据出处行锚」「真实入口判定口径」两条文(本批落);链式锚机检已在闸(capability_audit 链断 0,80 套件全承接)。剩余面=①营地七物件段等 ~40 处 emit 站点仍为接线验证(大门/进档链已于本批真输入化,能力行 :158 残险在案)②存量 996 条断言的判据出处逐条精锚(链式锚已保可核,精锚提升出处粒度)。 + +**缺口**:无(规则已立,纯执行分期;①依赖屏幕级真输入承载扩展)。 + +**改动面**:游戏代码 + +**状态**:待执行