pbl_blueprint/scripts/patch_selfcheck_f.py
2026-09-16 12:49:06 +08:00

116 lines
4.6 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- coding: utf-8 -*-
"""重写 selfcheck.py 的 F 组:改为 import load_path 模块读取权威 PATHS 元组。
旧实现用正则扫描 load_path.py **源码文本**,会把生成器表达式里的模板串
"/api/pbl_learner/%s.dspy" 误当成注册路径(实际 PATHS 里是展开后的字面量),
导致「无通配符」检查假失败。改为 importlib 加载模块、直接读 PATHS 与 validate()
以运行时真实值为准,彻底消除文本扫描误报。
"""
import os
import re
import shutil
import subprocess
import sys
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
SC = os.path.join(ROOT, "scripts", "selfcheck.py")
src = open(SC, "r", encoding="utf-8").read()
bak = SC + ".fbak"
shutil.copyfile(SC, bak)
NEW_F = '''# ============================================================== F. RBAC 路径
def group_f() -> None:
"""以 import load_path 后的运行时 PATHS 元组为权威(不扫源码文本,避免把
生成器模板串 '/api/x/%s.dspy' 误判为通配路径)。"""
g = "F.RBAC路径"
check(g, os.path.isfile(LOAD_PATH), "load_path.py 存在", rel(LOAD_PATH))
mod = None
err = ""
try:
import importlib.util
spec = importlib.util.spec_from_file_location("_lp_m1a", LOAD_PATH)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
except Exception as e: # noqa: BLE001
err = "%s: %s" % (type(e).__name__, e)
check(g, mod is not None, "load_path.py 可导入执行", err or "导入成功")
if mod is None:
return
paths = list(getattr(mod, "PATHS", ()) or ())
check(g, len(paths) > 0, "RBAC 注册路径显式枚举PATHS 元组)",
"路径数=%d" % len(paths))
wild = [p for p in paths if "%" in p or "*" in p]
check(g, not wild, "RBAC 路径无 %/* 通配符(运行时真实值)",
"通配路径=%s" % (wild[:5] or ""))
nonstd = [p for p in paths
if not re.fullmatch(r"/api/[A-Za-z0-9_.-]+\\.dspy", p)]
check(g, not nonstd, "RBAC 路径均为 /api/<表>/<动作>.dspy 规范字面量",
"异常=%s" % (nonstd[:5] or ""))
dup = sorted({p for p in paths if paths.count(p) > 1})
check(g, not dup, "RBAC 路径无重复", "重复=%s" % (dup[:5] or ""))
# 覆盖 json/*.json editable 的 3×11 条(超集合法:支撑表含额外动作)
need = set()
if os.path.isdir(JSON_DIR):
for fn in sorted(os.listdir(JSON_DIR)):
if not fn.endswith(".json"):
continue
d, _e = load_json(os.path.join(JSON_DIR, fn))
for u in ((d or {}).get("editable") or []):
if isinstance(u, str) and u.startswith("api/"):
need.add("/" + u)
missing = sorted(need - set(paths))
check(g, not missing,
"load_path.py 覆盖 json/*.json editable 全部路径(超集)",
"load_path=%d json_editable=%d 未覆盖=%s"
% (len(paths), len(need), missing[:5] or ""))
# 孤儿路径:每条路径的表名必须归属主清单 11 表或支撑表 9 表
main_t = set(getattr(mod, "MAIN_TABLES", ()) or ())
sup_t = set(getattr(mod, "SUPPORT_TABLES", ()) or ())
known = main_t | sup_t
check(g, len(main_t) == 11, "load_path.MAIN_TABLES = 设计定稿 11 表",
"实际=%d" % len(main_t))
orphan = []
for p in paths:
m = re.match(r"/api/([a-z0-9_]+)/", p)
if not m or (known and m.group(1) not in known):
orphan.append(p)
check(g, not orphan, "load_path.py 无孤儿路径(均归属已知表)",
"孤儿=%s" % (orphan[:5] or ""))
# 模块自带 validate() 必须通过
vfn = getattr(mod, "validate", None)
if callable(vfn):
verrs = vfn() or []
check(g, not verrs, "load_path.validate() 自检通过",
"错误=%s" % (verrs[:3] or ""))
else:
check(g, False, "load_path.validate() 存在", "未定义 validate()")
'''
pat = re.compile(r"# =+ F\. RBAC 路径\n.*?(?=# =+ G\.)", re.S)
if pat.search(src):
src = pat.sub(NEW_F, src, count=1)
open(SC, "w", encoding="utf-8").write(src)
r = subprocess.run([sys.executable, "-m", "py_compile", SC],
capture_output=True, text=True)
if r.returncode != 0:
shutil.move(bak, SC)
print("[ROLLBACK] 编译失败已回滚: %s" % (r.stderr or "")[-400:])
sys.exit(1)
os.remove(bak)
print("[FIX ] F 组已重写为 import load_path 读运行时 PATHS")
else:
os.remove(bak)
print("[ERR ] 未匹配到 F 组区块,未修改")
sys.exit(1)