# -*- 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/.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/.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 "")) 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"]