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:
parent
b4c2ecbbe1
commit
2e8d2abec7
@ -108,6 +108,25 @@ def build_package_map(space_dir):
|
|||||||
return pkgs
|
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):
|
def _resolve_module_file(mod, pkgs):
|
||||||
"""绝对模块名 → 文件路径;'EXT'=外部包;None=包内但文件不存在。"""
|
"""绝对模块名 → 文件路径;'EXT'=外部包;None=包内但文件不存在。"""
|
||||||
top = mod.split(".")[0]
|
top = mod.split(".")[0]
|
||||||
@ -263,35 +282,61 @@ class _ClosureChecker:
|
|||||||
yield os.path.join(root, fn)
|
yield os.path.join(root, fn)
|
||||||
|
|
||||||
|
|
||||||
def check_closure_for_delivery(space_dir, written_files, max_problems=30):
|
def _repo_of(path, repos):
|
||||||
"""deliver 门禁用:核验范围=本任务写入 .py 所属包 + 全空间引用这些包的文件。
|
"""文件归属仓库名(最长前缀匹配)。"""
|
||||||
|
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:
|
try:
|
||||||
ck = _ClosureChecker(space_dir)
|
ck = _ClosureChecker(space_dir)
|
||||||
if not ck.pkgs:
|
repos = build_repo_map(space_dir)
|
||||||
|
if not repos:
|
||||||
return [], set()
|
return [], set()
|
||||||
# 本任务写入 .py 所属包
|
# 本任务写入 .py 所属仓库
|
||||||
involved = set()
|
involved = set()
|
||||||
for f in dict.fromkeys(written_files or []):
|
for f in dict.fromkeys(written_files or []):
|
||||||
if not f.endswith(".py"):
|
if not f.endswith(".py"):
|
||||||
continue
|
continue
|
||||||
po = ck._pkg_of_file(f)
|
rn = _repo_of(f, repos)
|
||||||
if po:
|
if rn:
|
||||||
involved.add(po[0])
|
involved.add(rn)
|
||||||
if not involved:
|
if not involved:
|
||||||
return [], set()
|
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()
|
targets = set()
|
||||||
for py in ck.all_py_files():
|
for py in ck.all_py_files():
|
||||||
po = ck._pkg_of_file(py)
|
rn = _repo_of(py, repos)
|
||||||
if po and po[0] in involved:
|
if rn in involved:
|
||||||
targets.add(py)
|
targets.add(py)
|
||||||
continue
|
continue
|
||||||
for _node, mod in ck.iter_imports(py):
|
for _node, mod in ck.iter_imports(py):
|
||||||
if mod.split(".")[0] in involved:
|
if mod.split(".")[0] in owned_pkgs:
|
||||||
targets.add(py)
|
targets.add(py)
|
||||||
break
|
break
|
||||||
problems = []
|
problems = []
|
||||||
@ -304,7 +349,7 @@ def check_closure_for_delivery(space_dir, written_files, max_problems=30):
|
|||||||
if k not in seen:
|
if k not in seen:
|
||||||
seen.add(k)
|
seen.add(k)
|
||||||
uniq.append(p)
|
uniq.append(p)
|
||||||
return uniq[:max_problems], involved
|
return uniq[:max_problems], involved | owned_pkgs
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning("import closure check failed (escape, deliver 放行): %r", e)
|
logger.warning("import closure check failed (escape, deliver 放行): %r", e)
|
||||||
return None, set()
|
return None, set()
|
||||||
|
|||||||
@ -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")])
|
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))
|
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")
|
print("\nFAILS:", FAILS or "NONE")
|
||||||
sys.exit(1 if FAILS else 0)
|
sys.exit(1 if FAILS else 0)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user