337 lines
14 KiB
Python
337 lines
14 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""pbl_validation 模块挂载入口。
|
||
|
||
``load_pbl_validation()`` 在应用 ``app/pbls.py`` 的 ``init()`` 中按序调用,完成:
|
||
|
||
1. 库名解析:``ServerEnv().get_module_dbname('pbl_validation')``(禁止硬编码 DBNAME);
|
||
2. 建表:``pbl_validation_rule`` / ``pbl_validation_result``(幂等,存在即跳过);
|
||
3. 契约注册:5 个 ``/pbl_validation/api/<name>.dspy``;
|
||
4. 内置 14 维规则种子(幂等,租户级按需写入)。
|
||
|
||
所有步骤都做了容错:平台对象缺失时记录 warning 而不抛异常,保证应用可启动;
|
||
但**契约函数本身**在运行期缺 tenant_id / DBNAME 时严格 fail-closed。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import time
|
||
from typing import Any, Dict, List, Optional
|
||
|
||
from .api import API_REGISTRY, MODULE_NAME, TBL_RESULT, TBL_RULE, builtin_rules
|
||
from .constants import BUILTIN_RULESET, ENGINE_VERSION
|
||
from .dimensions import DIM_COUNT
|
||
|
||
_LOADED = False
|
||
_WARNINGS: List[str] = []
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 表定义(四段式:summary / fields / indexes / codes)
|
||
# ---------------------------------------------------------------------------
|
||
TABLES: Dict[str, Dict[str, Any]] = {
|
||
TBL_RULE: {
|
||
"summary": "校验规则(14 维,规则 JSON 形态 script_type=1,阈值配置化)",
|
||
"fields": [
|
||
("tenant_id", "varchar(64)", "not null", "租户ID(强制打头)"),
|
||
("rule_id", "varchar(64)", "not null", "规则ID"),
|
||
("dim", "varchar(8)", "not null", "维度编码 D01~D14"),
|
||
("dim_code", "varchar(64)", "", "维度语义码 task_chain 等"),
|
||
("name", "varchar(128)", "", "规则名称"),
|
||
("description", "varchar(512)", "", "规则说明"),
|
||
("weight", "decimal(6,3)", "default 1.000", "维度权重"),
|
||
("severity", "varchar(16)", "default 'major'", "blocker/major/minor/info"),
|
||
("script_type", "int", "default 1", "1=规则JSON形态"),
|
||
("ruleset", "varchar(64)", "", "规则集编码"),
|
||
("enabled", "int", "default 1", "1启用 0停用"),
|
||
("builtin", "int", "default 0", "1=内置规则"),
|
||
("rule_json", "text", "", "规则JSON(含 thresholds 覆盖)"),
|
||
("created_at", "bigint", "default 0", "创建时间(ms)"),
|
||
("updated_at", "bigint", "default 0", "更新时间(ms)"),
|
||
],
|
||
"indexes": [
|
||
("pk_%s" % TBL_RULE, "primary key", "rule_id"),
|
||
("idx_%s_tenant_dim" % TBL_RULE, "index", "tenant_id, dim"),
|
||
("idx_%s_tenant_enabled" % TBL_RULE, "index", "tenant_id, enabled"),
|
||
],
|
||
"codes": {"severity": ["blocker", "major", "minor", "info"],
|
||
"enabled": [0, 1], "script_type": [1]},
|
||
},
|
||
TBL_RESULT: {
|
||
"summary": "校验结果(14 维得分 + 5 级质量状态 + 完整报告 JSON)",
|
||
"fields": [
|
||
("tenant_id", "varchar(64)", "not null", "租户ID(强制打头)"),
|
||
("result_id", "varchar(64)", "not null", "结果ID vr_xxx"),
|
||
("blueprint_id", "varchar(64)", "", "蓝图ID"),
|
||
("version", "varchar(32)", "", "蓝图版本"),
|
||
("ruleset", "varchar(64)", "", "规则集编码"),
|
||
("quality_state", "varchar(24)", "", "Q0_DRAFT~Q4_EXCELLENT"),
|
||
("quality_label", "varchar(32)", "", "质量状态中文标签"),
|
||
("total_score", "decimal(6,4)", "default 0", "加权总分 0~1"),
|
||
("dim_count", "int", "default 0", "实际执行维度数"),
|
||
("pass_count", "int", "default 0", "pass 维度数"),
|
||
("warn_count", "int", "default 0", "warn 维度数"),
|
||
("fail_count", "int", "default 0", "fail 维度数"),
|
||
("skip_count", "int", "default 0", "skip 维度数"),
|
||
("blocker_count", "int", "default 0", "blocker 问题数"),
|
||
("major_count", "int", "default 0", "major 问题数"),
|
||
("minor_count", "int", "default 0", "minor 问题数"),
|
||
("info_count", "int", "default 0", "info 问题数"),
|
||
("issue_count", "int", "default 0", "问题总数"),
|
||
("dim_scores", "text", "", "各维度状态 JSON"),
|
||
("dim_detail", "text", "", "各维度得分/指标 JSON"),
|
||
("fingerprint", "varchar(64)", "", "载荷+阈值指纹(幂等)"),
|
||
("engine_version", "varchar(16)", "", "引擎版本"),
|
||
("contract", "varchar(48)", "", "报告契约版本"),
|
||
("report_json", "longtext", "", "完整校验报告 JSON"),
|
||
("elapsed_ms", "int", "default 0", "执行耗时(ms)"),
|
||
("operator", "varchar(64)", "", "操作人"),
|
||
("state", "varchar(16)", "default 'done'", "done/failed"),
|
||
("created_at", "bigint", "default 0", "创建时间(ms)"),
|
||
("updated_at", "bigint", "default 0", "更新时间(ms)"),
|
||
],
|
||
"indexes": [
|
||
("pk_%s" % TBL_RESULT, "primary key", "result_id"),
|
||
("idx_%s_tenant_bp" % TBL_RESULT, "index", "tenant_id, blueprint_id"),
|
||
("idx_%s_tenant_quality" % TBL_RESULT, "index", "tenant_id, quality_state"),
|
||
("idx_%s_tenant_created" % TBL_RESULT, "index", "tenant_id, created_at"),
|
||
("idx_%s_fingerprint" % TBL_RESULT, "index", "tenant_id, fingerprint"),
|
||
],
|
||
"codes": {"quality_state": ["Q0_DRAFT", "Q1_INCOMPLETE", "Q2_BASIC",
|
||
"Q3_GOOD", "Q4_EXCELLENT"],
|
||
"state": ["done", "failed"]},
|
||
},
|
||
}
|
||
|
||
_TYPE_MAP = {
|
||
"varchar": "VARCHAR", "int": "INT", "bigint": "BIGINT",
|
||
"decimal": "DECIMAL", "text": "TEXT", "longtext": "LONGTEXT",
|
||
}
|
||
|
||
|
||
def _warn(msg: str) -> None:
|
||
if msg not in _WARNINGS:
|
||
_WARNINGS.append(msg)
|
||
try:
|
||
print("[pbl_validation][WARN] %s" % msg)
|
||
except Exception:
|
||
pass
|
||
|
||
|
||
def get_dbname() -> str:
|
||
"""解析模块库名(唯一合法来源:ServerEnv 映射,禁止硬编码)。"""
|
||
ServerEnv = None
|
||
try:
|
||
from sage.platform import ServerEnv # type: ignore
|
||
except Exception:
|
||
try:
|
||
from ahserver.serverenv import ServerEnv # type: ignore
|
||
except Exception:
|
||
ServerEnv = None
|
||
if ServerEnv is None:
|
||
raise RuntimeError("ServerEnv 不可用,无法解析 pbl_validation 库名")
|
||
dbname = ServerEnv().get_module_dbname(MODULE_NAME)
|
||
if not dbname:
|
||
raise RuntimeError(
|
||
"ServerEnv().get_module_dbname('%s') 为空:请在应用入口 get_module_dbname "
|
||
"映射中登记 pbl_validation" % MODULE_NAME)
|
||
return dbname
|
||
|
||
|
||
def _sqlor():
|
||
try:
|
||
import sqlor # type: ignore
|
||
return sqlor
|
||
except Exception as exc:
|
||
raise RuntimeError("sqlor 不可用:%s" % exc)
|
||
|
||
|
||
def ddl_statements(dbtype: str = "mysql") -> List[str]:
|
||
"""生成建表 DDL(幂等:IF NOT EXISTS)。供 load 与 sql/ 目录落盘共用。"""
|
||
out: List[str] = []
|
||
for tbl, spec in TABLES.items():
|
||
cols = []
|
||
for name, typ, extra, _comment in spec["fields"]:
|
||
base = _TYPE_MAP.get(typ.split("(")[0].lower(), "VARCHAR(255)")
|
||
if "(" in typ:
|
||
base = "%s(%s)" % (base, typ.split("(", 1)[1].rstrip(")"))
|
||
piece = "`%s` %s" % (name, base)
|
||
if extra:
|
||
piece = "%s %s" % (piece, extra.replace("default ", "DEFAULT ")
|
||
.replace("not null", "NOT NULL"))
|
||
cols.append(piece)
|
||
for idx_name, idx_kind, idx_cols in spec["indexes"]:
|
||
col_sql = ", ".join("`%s`" % c.strip() for c in idx_cols.split(","))
|
||
if idx_kind == "primary key":
|
||
cols.append("PRIMARY KEY (%s)" % col_sql)
|
||
else:
|
||
cols.append("KEY `%s` (%s)" % (idx_name, col_sql))
|
||
out.append("CREATE TABLE IF NOT EXISTS `%s` (\n %s\n) ENGINE=InnoDB "
|
||
"DEFAULT CHARSET=utf8mb4 COMMENT='%s'"
|
||
% (tbl, ",\n ".join(cols), spec["summary"]))
|
||
return out
|
||
|
||
|
||
def ensure_tables(dbname: Optional[str] = None) -> List[str]:
|
||
"""幂等建表,返回执行过的 DDL。"""
|
||
dbname = dbname or get_dbname()
|
||
sor = _sqlor()
|
||
executed = []
|
||
for sql in ddl_statements():
|
||
try:
|
||
if hasattr(sor, "sqlExe"):
|
||
sor.sqlExe(sql, [], dbname=dbname) \
|
||
if _accepts_dbname(sor.sqlExe) else sor.sqlExe(sql, [])
|
||
executed.append(sql.split("\n")[0])
|
||
except Exception as exc:
|
||
_warn("建表失败(可能已存在):%s" % exc)
|
||
return executed
|
||
|
||
|
||
def _accepts_dbname(func) -> bool:
|
||
try:
|
||
import inspect
|
||
return "dbname" in inspect.signature(func).parameters
|
||
except Exception:
|
||
return False
|
||
|
||
|
||
def seed_builtin_rules(dbname: Optional[str] = None,
|
||
tenant_id: Optional[str] = None) -> int:
|
||
"""幂等写入内置 14 维规则种子(tenant_id 为空时写全局占位 '')。"""
|
||
dbname = dbname or get_dbname()
|
||
sor = _sqlor()
|
||
tid = tenant_id or ""
|
||
now = int(time.time() * 1000)
|
||
seeded = 0
|
||
for r in builtin_rules():
|
||
r["tenant_id"] = tid
|
||
try:
|
||
if hasattr(sor, "R"):
|
||
exist = sor.R(TBL_RULE,
|
||
{"tenant_id": tid, "rule_id": r["rule_id"]},
|
||
dbname=dbname) if _accepts_dbname(sor.R) \
|
||
else sor.R(TBL_RULE, {"tenant_id": tid, "rule_id": r["rule_id"]})
|
||
if exist:
|
||
continue
|
||
row = {k: r.get(k) for k in
|
||
("tenant_id", "rule_id", "dim", "dim_code", "name", "description",
|
||
"weight", "severity", "script_type", "ruleset", "enabled",
|
||
"builtin", "rule_json")}
|
||
row["created_at"] = now
|
||
row["updated_at"] = now
|
||
if hasattr(sor, "C"):
|
||
sor.C(TBL_RULE, row, dbname=dbname) if _accepts_dbname(sor.C) \
|
||
else sor.C(TBL_RULE, row)
|
||
seeded += 1
|
||
except Exception as exc:
|
||
_warn("内置规则种子写入失败 %s:%s" % (r.get("rule_id"), exc))
|
||
return seeded
|
||
|
||
|
||
def register_apis(env: Any = None) -> List[str]:
|
||
"""注册 5 个契约接口到 ServerEnv(路径 /pbl_validation/api/<name>.dspy)。"""
|
||
paths = []
|
||
for name, func in API_REGISTRY.items():
|
||
path = "/%s/api/%s.dspy" % (MODULE_NAME, name)
|
||
paths.append(path)
|
||
if env is None:
|
||
continue
|
||
for register in ("register_api", "add_api", "register_dspy", "api"):
|
||
fn = getattr(env, register, None)
|
||
if callable(fn):
|
||
try:
|
||
fn(path, func)
|
||
break
|
||
except TypeError:
|
||
try:
|
||
fn(name, func)
|
||
break
|
||
except Exception as exc:
|
||
_warn("契约注册失败 %s:%s" % (path, exc))
|
||
except Exception as exc:
|
||
_warn("契约注册失败 %s:%s" % (path, exc))
|
||
return paths
|
||
|
||
|
||
def load_pbl_validation(env: Any = None, with_seed: bool = True) -> Dict[str, Any]:
|
||
"""模块挂载入口(应用 init() 中调用)。
|
||
|
||
:param env: ServerEnv 实例(可选,缺省自行构造)
|
||
:param with_seed: 是否写入内置 14 维规则种子
|
||
:return: 挂载信息 dict(dbname / tables / apis / dim_count / warnings)
|
||
"""
|
||
global _LOADED
|
||
info: Dict[str, Any] = {
|
||
"module": MODULE_NAME,
|
||
"engine_version": ENGINE_VERSION,
|
||
"ruleset": BUILTIN_RULESET,
|
||
"dim_count": DIM_COUNT,
|
||
"tables": [TBL_RULE, TBL_RESULT],
|
||
"apis": [],
|
||
"dbname": "",
|
||
"seeded": 0,
|
||
"loaded_before": _LOADED,
|
||
"warnings": [],
|
||
}
|
||
|
||
if env is None:
|
||
try:
|
||
from sage.platform import ServerEnv # type: ignore
|
||
env = ServerEnv()
|
||
except Exception:
|
||
try:
|
||
from ahserver.serverenv import ServerEnv # type: ignore
|
||
env = ServerEnv()
|
||
except Exception:
|
||
env = None
|
||
|
||
try:
|
||
dbname = get_dbname()
|
||
info["dbname"] = dbname
|
||
except Exception as exc:
|
||
_warn("库名解析失败:%s" % exc)
|
||
dbname = None
|
||
|
||
info["apis"] = register_apis(env)
|
||
|
||
if dbname:
|
||
try:
|
||
info["ddl"] = ensure_tables(dbname)
|
||
except Exception as exc:
|
||
_warn("建表跳过:%s" % exc)
|
||
if with_seed:
|
||
try:
|
||
info["seeded"] = seed_builtin_rules(dbname)
|
||
except Exception as exc:
|
||
_warn("规则种子跳过:%s" % exc)
|
||
|
||
_LOADED = True
|
||
info["warnings"] = list(_WARNINGS)
|
||
try:
|
||
print("[pbl_validation] loaded: %d dims, %d apis, db=%s"
|
||
% (DIM_COUNT, len(info["apis"]), info["dbname"] or "<unresolved>"))
|
||
except Exception:
|
||
pass
|
||
return info
|
||
|
||
|
||
def module_manifest() -> Dict[str, Any]:
|
||
"""模块自描述(供应用启动自检 / 文档生成)。"""
|
||
return {
|
||
"module": MODULE_NAME,
|
||
"version": ENGINE_VERSION,
|
||
"tables": {t: TABLES[t]["summary"] for t in TABLES},
|
||
"apis": ["/%s/api/%s.dspy" % (MODULE_NAME, n) for n in API_REGISTRY],
|
||
"dimensions": [
|
||
{"dim": m["dim"], "code": m["code"], "name": m["name"],
|
||
"weight": m["weight"]}
|
||
for m in __import__("pbl_validation.dimensions", fromlist=["x"]).DIMENSION_REGISTRY
|
||
],
|
||
"quality_states": ["Q0_DRAFT", "Q1_INCOMPLETE", "Q2_BASIC",
|
||
"Q3_GOOD", "Q4_EXCELLENT"],
|
||
}
|
||
|
||
|
||
__all__ = ["load_pbl_validation", "get_dbname", "ensure_tables", "ddl_statements",
|
||
"seed_builtin_rules", "register_apis", "module_manifest", "TABLES"]
|