diff --git a/pipeline_service/import_closure.py b/pipeline_service/import_closure.py index ff81cd6..6c4daff 100644 --- a/pipeline_service/import_closure.py +++ b/pipeline_service/import_closure.py @@ -15,9 +15,18 @@ ImportError,公共内核对外契约瘫痪。设计文档明文「接口变更 2. 对每个 .py 收集模块级名字绑定(def/class/赋值/import 别名,含 If/Try 分支),对每条 from-import 核验符号在目标模块有定义(含再导出/子模块)。 3. 逃逸阀(防误报死锁):目标模块定义 module-level __getattr__(PEP 562 - 动态属性)→ 跳过符号级核验;ast 解析失败跳过(语法归空壳门禁);外部库 + 动态属性)或 module-level `globals()[...] = ...` 运行时符号注入(2026-09-17 + pbls M1b 死循环根治:agent 用工具脚本生成 compat 注入块修断裂,运行时 + import 全通但 ast 量不出下标赋值 → 门禁恒报断裂 → QC 判「声称修复 vs + 实测断裂」矛盾 → 无限退回;与 PEP 562 同类,静态不可判定即保守放行) + → 跳过该模块符号级核验;ast 解析失败跳过(语法归空壳门禁);外部库 (顶层名不在包映射)不核验;本核验器自身异常 → 返回 None 放行(记日志)。 +责任范围过滤(2026-09-17 同日修正):依赖方向拉进来的外部文件,只拦 +「引用涉及包」的断裂;它引用其他包(如半迁移欠账 pbl_common)的存量断裂 +不是本任务责任,不进门禁清单——否则小任务背上全空间欠账,永远修不完 +(pbls M1b 实测:26 处里 6 处是 pbl_agent_runtime→pbl_common 存量断裂)。 + 门禁范围(check_closure_for_delivery): - 本任务写入 .py 所属的包 = 涉及包;核验对象 = 涉及包内全部文件 + 全空间 import 涉及包的文件。改公共包断掉依赖方(事故形状)会在 deliver 当轮被拦, @@ -45,11 +54,16 @@ def _add_target(t, names): def _collect_defs(tree): - """模块级名字绑定集合 + 是否定义 __getattr__ + 星号导入 (module, level) 列表。""" + """模块级名字绑定集合 + 是否定义 __getattr__/globals()注入 + 星号导入列表。""" names = set() dynamic = False stars = [] + def _is_ns_call(node): + """globals()/vars()/locals() 调用形态(运行时符号注入,静态不可判定)。""" + return (isinstance(node, ast.Call) and isinstance(node.func, ast.Name) + and node.func.id in ("globals", "vars", "locals")) + def walk_top(stmts): nonlocal dynamic for s in stmts: @@ -61,6 +75,19 @@ def _collect_defs(tree): elif isinstance(s, ast.Assign): for t in s.targets: _add_target(t, names) + # globals()[name] = val / vars()[...] = ... 运行时注入: + # 符号面静态不可判定 → 整模块按动态放行(同 PEP 562 逃逸阀) + if (isinstance(t, ast.Subscript) and _is_ns_call(t.value)): + dynamic = True + if _is_ns_call(s.value): + # X = globals() 之后再 X[...] = ...:保守也视为注入形态 + dynamic = True + elif isinstance(s, ast.Expr) and isinstance(s.value, ast.Call): + # globals().update({...}) 形态 + fn = s.value.func + if (isinstance(fn, ast.Attribute) and fn.attr == "update" + and _is_ns_call(fn.value)): + dynamic = True elif isinstance(s, ast.AnnAssign): if s.target is not None: _add_target(s.target, names) @@ -342,6 +369,13 @@ def check_closure_for_delivery(space_dir, written_files, max_problems=30): problems = [] for f in sorted(targets): problems.extend(ck.check_file(f)) + # 责任范围过滤(2026-09-17 pbls M1b 死循环修正):只保留「断裂源在 + # 涉及仓库自有包」的问题——即本任务改动面自己的契约断裂(含依赖方 + # import 涉及包断裂 = pbl_common 事故形状)。断裂源在其他包(如 + # pbl_common 半迁移存量欠账)的不是本任务责任:小任务修不完全空间 + # 欠账,硬拦只会无限退回(实测 26 处里 6 处是别的包的存量账)。 + problems = [p for p in problems + if p.get("source", "").split(".")[0] in owned_pkgs] # 去重 seen, uniq = set(), [] for p in problems: