diff --git a/game/tests/unit/contracts/source_guard_test.gd b/game/tests/unit/contracts/source_guard_test.gd index 394ab682..276cc2fc 100644 --- a/game/tests/unit/contracts/source_guard_test.gd +++ b/game/tests/unit/contracts/source_guard_test.gd @@ -10,6 +10,12 @@ extends SceneTree const Kit := preload("res://tests/unit/test_kit.gd") +# V1d 层序单口豁免(组件内部自身布局重排,非根容器层序):文件 → 允许 move_child 的函数名。 +# 登记面封闭——未登记的函数内出现 move_child 一律判红,新增文件默认受管。 +const LAYER_REORDER_EXEMPT: Dictionary = { + "res://presentation/views/ui_kit.gd": "add_footer", +} + func _init() -> void: # 套执行序沿源编排序(_split_tail 尾段调用序) @@ -68,23 +74,39 @@ func _smoke_hygiene_gates() -> void: var mv: String = FileAccess.get_file_as_string("res://presentation/main.gd") Kit.check("V8 GP 影子账残留零命中(main 源码面——文件已删+委托缝已拆)〔迭代27 T3〕", not mv.contains("gp_shadow_ledger") and not mv.contains("_gp_ledger")) - # V1d 层序落位单口:注册面建件后扫全 presentation——move_child 只允许出现在 ui_registry.gd 的 - # apply_layer_contract 函数体内(合同装配单口——出现第二处落位=硬编码层序,须改合同表而非加调用) + # V1d 层序落位单口:递归扫全 presentation(含 views/、scenes/)——move_child 只允许出现在 + # ui_registry.gd 的 apply_layer_contract 函数体内,或 LAYER_REORDER_EXEMPT 登记的组件内重排函数内 + # (合同装配单口——出现第二处落位=硬编码层序,须改合同表而非加调用) var scanned: int = 0 var outside_calls: int = 0 for fn_path: String in _list_scripts("res://presentation"): var src: String = FileAccess.get_file_as_string(fn_path) scanned += 1 - var fn_start: int = src.find("func apply_layer_contract") if fn_path.contains("ui_registry.gd") else -1 - var fn_end: int = src.find("\nfunc ", fn_start + 10) if fn_start >= 0 else -1 - if fn_end < 0: - fn_end = src.length() - outside_calls += (src.substr(0, fn_start) + src.substr(fn_end)).count("move_child(") - Kit.check("V1d 层序落位单口(presentation %d 文件——apply_layer_contract 之外 move_child 调用点=%d 应=0)〔注册面 V1〕" % [scanned, outside_calls], + outside_calls += _count_outside_allowed(src, fn_path) + Kit.check("V1d 层序落位单口(presentation %d 文件——落位单口与登记豁免之外 move_child 调用点=%d 应=0)〔注册面 V1〕" % [scanned, outside_calls], scanned > 0 and outside_calls == 0) -# 目录 .gd 清单(DirAccess 平铺——presentation 无子目录;扫描面>0 前置由调用侧断言承载) +# 允许段之外逐个文件数 move_child:遇任意函数声明行重判「当前是否在允许函数内」, +# 内部类方法与缩进形态都不影响定位;remove_child( 尾部与 move_child( 字面重合,先剔除再计数。 +static func _count_outside_allowed(src: String, fn_path: String) -> int: + var allowed: PackedStringArray = PackedStringArray() + if fn_path.ends_with("ui_registry.gd"): + allowed.append("apply_layer_contract") + if LAYER_REORDER_EXEMPT.has(fn_path): + allowed.append(String(LAYER_REORDER_EXEMPT[fn_path])) + var in_allowed: bool = false + var calls: int = 0 + for line: String in src.split("\n"): + var t: String = line.strip_edges() + if t.begins_with("func "): + in_allowed = allowed.has(t.substr(5).split("(")[0].strip_edges()) + if not in_allowed: + calls += line.replace("remove_child(", "").count("move_child(") + return calls + + +# 目录 .gd 清单(DirAccess 递归——presentation 下 views/、scenes/ 等子目录同样在册;扫描面>0 前置由调用侧断言承载) static func _list_scripts(dir_path: String) -> PackedStringArray: var out: PackedStringArray = PackedStringArray() var dir := DirAccess.open(dir_path) @@ -93,7 +115,9 @@ static func _list_scripts(dir_path: String) -> PackedStringArray: dir.list_dir_begin() var fname := dir.get_next() while fname != "": - if fname.ends_with(".gd"): + if dir.current_is_dir() and not fname.begins_with("."): + out.append_array(_list_scripts(dir_path + "/" + fname)) + elif fname.ends_with(".gd"): out.append(dir_path + "/" + fname) fname = dir.get_next() dir.list_dir_end()