fix(import-closure): 核验范围从包归属改为仓库归属——modules/pbls只有app/scripts没有包目录,入口modules/pbls/app/pbls.py的import断裂(from world.api实测断)是真实事故面,只按包算会漏;跨仓库方向按写入文件所属仓库拥有的包算(pbl_common事故形状不变)。单测+3例=14全绿(含无包仓库入口纳入/断裂抓到/存在不误报)

This commit is contained in:
ymq 2026-09-17 11:42:10 +08:00
parent b4c2ecbbe1
commit 2e8d2abec7
2 changed files with 70 additions and 13 deletions

View File

@ -108,6 +108,25 @@ def build_package_map(space_dir):
return pkgs
def build_repo_map(space_dir):
"""modules/<repo> 与 apps/<repo> 的仓库根映射:[(仓库绝对路径, 仓库名)]。
仓库 modules/pbls/ 只有 app/ scripts/ 没有包目录但入口文件
modules/pbls/app/pbls.py import 断裂同样是真实事故面pbls 入口
from world.api import 实测断裂门禁范围必须按仓库归属算不能只按包
"""
repos = []
for group in ("modules", "apps"):
base = os.path.join(space_dir, group)
if not os.path.isdir(base):
continue
for repo in sorted(os.listdir(base)):
repo_dir = os.path.join(base, repo)
if os.path.isdir(repo_dir) and not repo.startswith("."):
repos.append((os.path.abspath(repo_dir), repo))
return repos
def _resolve_module_file(mod, pkgs):
"""绝对模块名 → 文件路径;'EXT'=外部包None=包内但文件不存在。"""
top = mod.split(".")[0]
@ -263,35 +282,61 @@ class _ClosureChecker:
yield os.path.join(root, fn)
def check_closure_for_delivery(space_dir, written_files, max_problems=30):
"""deliver 门禁用:核验范围=本任务写入 .py 所属包 + 全空间引用这些包的文件。
def _repo_of(path, repos):
"""文件归属仓库名(最长前缀匹配)。"""
ap = os.path.abspath(path)
best = None
for repo_dir, name in repos:
if ap.startswith(repo_dir + os.sep):
if best is None or len(repo_dir) > len(best[0]):
best = (repo_dir, name)
return best[1] if best else None
返回 (problems, involved_pkgs)核验器自身异常返回 (None, set())逃逸阀
def check_closure_for_delivery(space_dir, written_files, max_problems=30):
"""deliver 门禁用:核验范围=本任务写入 .py 所属【仓库】+ 全空间引用这些仓库的文件。
仓库归属而非包归属2026-09-16 修正modules/pbls 只有 app/ scripts/
没有包目录但入口 modules/pbls/app/pbls.py `from world.api import`
断裂同样是真实事故面只按包算会漏掉跨仓库方向写入文件所属仓库
拥有的包包根在仓库目录下被别处 import 断裂也拦pbl_common 事故形状
返回 (problems, involved_names)核验器自身异常返回 (None, set())逃逸阀
调用方放行并记日志门禁故障不阻断交付但必须在日志可见
"""
try:
ck = _ClosureChecker(space_dir)
if not ck.pkgs:
repos = build_repo_map(space_dir)
if not repos:
return [], set()
# 本任务写入 .py 所属包
# 本任务写入 .py 所属仓库
involved = set()
for f in dict.fromkeys(written_files or []):
if not f.endswith(".py"):
continue
po = ck._pkg_of_file(f)
if po:
involved.add(po[0])
rn = _repo_of(f, repos)
if rn:
involved.add(rn)
if not involved:
return [], set()
# 核验对象:涉及包内全部文件 + 全空间 import 涉及包的文件
# 涉及仓库拥有的包(跨仓库引用方向)
repo_dirs = {name: rd for rd, name in repos if name in involved}
owned_pkgs = set()
for pkg_name, pkg_root in ck.pkgs.items():
pkg_abs = os.path.abspath(os.path.join(pkg_root, pkg_name))
for rn, rd in repo_dirs.items():
if pkg_abs.startswith(rd + os.sep):
owned_pkgs.add(pkg_name)
break
# 核验对象:涉及仓库内全部文件 + 全空间 import 涉及包的文件
targets = set()
for py in ck.all_py_files():
po = ck._pkg_of_file(py)
if po and po[0] in involved:
rn = _repo_of(py, repos)
if rn in involved:
targets.add(py)
continue
for _node, mod in ck.iter_imports(py):
if mod.split(".")[0] in involved:
if mod.split(".")[0] in owned_pkgs:
targets.add(py)
break
problems = []
@ -304,7 +349,7 @@ def check_closure_for_delivery(space_dir, written_files, max_problems=30):
if k not in seen:
seen.add(k)
uniq.append(p)
return uniq[:max_problems], involved
return uniq[:max_problems], involved | owned_pkgs
except Exception as e:
logger.warning("import closure check failed (escape, deliver 放行): %r", e)
return None, set()

View File

@ -109,5 +109,17 @@ w("modules/pbl_rel/pbl_rel/bad.py", "from . import NOT_THERE\n")
probs8, _ = check_closure_for_delivery(ROOT, [os.path.join(ROOT, "modules/pbl_rel/pbl_rel/bad.py")])
check("相对导入断裂也抓", any(p["symbol"] == "NOT_THERE" for p in (probs8 or [])), str(probs8))
# ── 用例 8仓库归属——无包目录的仓库modules/pbls 形态)入口断裂也拦 ──
w("modules/pbls/app/pbls.py", "from world.api import load_world\nfrom pbl_common.kernel import PblError\n")
w("modules/world/world/__init__.py", "def load_world(env=None):\n return True\n")
w("modules/world/__init__.py", "from .world import load_world\n")
probs9, involved9 = check_closure_for_delivery(
ROOT, [os.path.join(ROOT, "modules/pbls/app/pbls.py")])
check("无包仓库入口纳入核验", "pbls" in involved9, str(involved9))
check("抓到 world.api 断裂", any(p["symbol"] == "world.api" for p in (probs9 or [])),
str(probs9))
check("pbl_common.kernel 存在不误报", not any("pbl_common.kernel" in str(p) for p in (probs9 or [])),
str(probs9))
print("\nFAILS:", FAILS or "NONE")
sys.exit(1 if FAILS else 0)