2026-09-17 23:45:03 +08:00

337 lines
14 KiB
Python
Raw 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 -*-
"""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: 挂载信息 dictdbname / 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"]