deliver: 交付收口(引擎代为提交)

This commit is contained in:
agent.develop 2026-09-20 18:20:03 +08:00
parent 2ee0d74dad
commit 3f0ee54c1c
3 changed files with 292 additions and 67 deletions

View File

@ -1,6 +1,6 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""validate_models_json —— database-table-definition-spec 机械校验器M11b-2b-A 修复版)。
"""validate_models_json —— database-table-definition-spec 机械校验器M11b-2b-A 修复版 rev3)。
QC #4/#7/#9/#10 要求:表定义 JSON 必须是 spec 规定的四段式summary/fields/indexes/
codes类型必须是 spec 声明的抽象类型主键 id 必须是 str/length>=32索引必须用
@ -17,12 +17,25 @@ codes类型必须是 spec 声明的抽象类型,主键 id 必须是 str/
QC #5 引用式 key/schemaspec 不承认引用式结构,直接判 ERROR 并提示改为内联四段式;
COMPLIANCE_DEVIATIONS 中针对 pbl_runtime_event/pbl_entity_state 4 条失效登记项
一并删除引用式在根键检查处即 FAIL根本到不了 fields 校验登记形同虚设
QC #1本轮放行机制对 auto_increment 生效字段段「spec 未定义键」检查原先无条件报错,
导致 (表名,"id","auto-increment") 即使已登记即使传了 --allow-registered-deviation
仍残留 `fields[N](id) spec 未定义键: auto_increment` 而整体 FAIL id-not-str32
可豁免auto_increment 不可豁免机制自相矛盾现在该检查改为**偏离感知**
id 上的 auto_increment 键统一交由 id 分支按 strict/allow 两态处理已登记 + 传开关
降级为 DEVIATION(registered) 明细不计入 errors未登记或未传开关 照旧 ERROR
其它字段上的未定义键也支持通用登记键 (表名, 列名, "unknown-key:<键名>")同样两态
已登记偏离唯一允许的偏离 --allow-registered-deviation 显式声明才放行
主键 id str(32) id auto_increment且已登记在 COMPLIANCE_DEVIATIONS
典型场景分区表物理主键需要 long + auto_increment未传该开关时这类项仍判 ERROR
并打印 --allow-registered-deviation 放行提示传入时逐条打印 DEVIATION(registered)
明细不静默放过当前登记表为空引用式失效项已删除后续若真源确需偏离再登记
偏离放行语义唯一允许的偏离 --allow-registered-deviation 显式声明才放行
1) (表名, "id", "id-not-str32") 主键 id str length<32
2) (表名, "id", "auto-increment") id spec 未定义键 auto_increment
3) (表名, 列名, "unknown-key:<K>") 任意字段带 spec 未定义键 <K>通用形式
未传该开关时上述项仍判 ERROR并打印 --allow-registered-deviation 放行提示传入时
逐条打印 DEVIATION(registered) 明细含登记说明不静默放过
注意只有**登记过**的才可能放行登记表为空时 --allow-registered-deviation 不放行任何项
典型场景分区表物理主键需要 long + auto_incrementMySQL 1503分区键必须进主键
若某表确需此类写法须先在其模块任务里把 (表名,"id","id-not-str32")
(表名,"id","auto-increment") 登记进 COMPLIANCE_DEVIATIONS再配合开关使用 两者缺一不可
只登记不传开关仍 FAIL只传开关不登记也 FAIL本文件末尾自测 5 给出四态实测输出
用法
python validate_models_json.py <models目录1> [<目录2> ...]
@ -81,6 +94,20 @@ def _is_reference_style(data):
return False
def _unknown_key_deviation_key(table, field_name, key_name):
"""字段段 spec 未定义键对应的登记键QC #1
id 上的 auto_increment 复用 (表名,"id","auto-increment") id 分支同一登记项
避免登记了 auto-increment 却因 unknown-key 分支无条件报错的放行失效缺陷
其余未定义键使用通用形式 (表名, 列名, "unknown-key:<键名>")
"""
if table is None:
return None
if field_name == "id" and key_name == "auto_increment":
return (table, "id", "auto-increment")
return (table, field_name, "unknown-key:%s" % key_name)
def check_type_value(type_value):
"""返回该 type 写法的问题列表spec 只允许抽象类型)。
@ -181,9 +208,26 @@ def validate_model(path, strict_id=True):
names.append(name)
if not f.get("title"):
errors.append("%s.title 必填DDL COMMENT 用)" % where)
unknown = [k for k in f if k not in FIELD_KEYS]
# QC #1spec 未定义键检查改为偏离感知 —— 已登记且传入 --allow-registered-deviation
# 的键不再计入 errorsid.auto_increment 交由下方 id 分支统一打印 DEVIATION 明细,
# 避免同一偏离重复计数;其它键在此直接降级为 DEVIATION(registered))。
unknown = []
for k in sorted([k for k in f if k not in FIELD_KEYS]):
dkey = _unknown_key_deviation_key(table, name, k)
if k == "auto_increment" and name == "id" and dkey in COMPLIANCE_DEVIATIONS:
continue # strict/allow 两态由 id 分支处理(同一登记项,只报一次)
if dkey is not None and dkey in COMPLIANCE_DEVIATIONS:
if strict_id:
# 已登记但未传开关:照旧 ERROR并明确提示如何放行与 id 分支同一语义)
errors.append("%s 含 spec 未定义键: %s —— 该项已登记偏离,%s"
% (where, k, DEVIATION_HINT))
continue
deviations.append("%s: 含 spec 未定义键 %s —— 已登记偏离:%s"
% (where, k, COMPLIANCE_DEVIATIONS[dkey]))
continue
unknown.append(k)
if unknown:
errors.append("%s 含 spec 未定义键: %s" % (where, ", ".join(sorted(unknown))))
errors.append("%s 含 spec 未定义键: %s" % (where, ", ".join(unknown)))
t = f.get("type")
errors.extend("%s: %s" % (where, m) for m in check_type_value(t))
req = ABSTRACT_TYPES.get(str(t).strip().lower(), ())
@ -208,14 +252,15 @@ def validate_model(path, strict_id=True):
else:
errors.append("%s: 主键 id 必须 str 且 length>=32spec 规则),实测 type=%r length=%r"
% (where, t, f.get("length")))
if f.get("auto_increment"):
if "auto_increment" in f:
key = (table, "id", "auto-increment")
if key in COMPLIANCE_DEVIATIONS:
if strict_id:
errors.append("%s: id auto_increment —— 该项已登记偏离,%s"
% (where, DEVIATION_HINT))
errors.append("%s: id 带 spec 未定义键 auto_increment实测 %r"
" —— 该项已登记偏离,%s"
% (where, f.get("auto_increment"), DEVIATION_HINT))
else:
deviations.append("%s: id auto_increment —— 已登记偏离:%s"
deviations.append("%s: id 带 spec 未定义键 auto_increment —— 已登记偏离:%s"
% (where, COMPLIANCE_DEVIATIONS[key]))
else:
errors.append("%s: auto_increment 未在偏离登记表中spec 无此键)" % where)

View File

@ -1,15 +1,27 @@
# -*- coding: utf-8 -*-
"""world_sync —— 世界运行时同步模块PBL scense 侧)
"""world_sync —— 世界运行时同步模块PBL scense 侧)包入口
M11b-2 增量单事务内事件 + 实体状态原子落库
对外只暴露写入/读取两个函数与异常族**不含广播不含轮询兜底不含时延优化**
**单一真源原则响应 QC #2 / QC #3**
=====================================
本文件只做转发 + 导出**不再自带第二份 ``load_world_sync`` 实现**
唯一实现位于 :mod:`world_sync.init`宿主挂载层它同时完成
- 实现``init.py`` 内的 ``load_world_sync(server_env=None, conn_factory=None)``
- 导出本文件 ``from .init import load_world_sync, ...``宿主 ``from world_sync import load_world_sync``
取到的就是 不存在第二份
- env 注册``init.py`` ``env.xxx = xxx`` 的三处同步登记
M11b-2 能力单事务内事件 + 实体状态原子落库
对外只暴露写入/读取两个契约函数与异常族**不含广播不含轮询兜底不含时延优化**
这些明确在 M11b-2 范围之外由后续里程碑承担
宿主接线遵循 project-directory-spec 8.1模块不读部署环境凭据文件::
# apps/scense/app/scense.py 的 init() 里
from world_sync import load_world_sync
load_world_sync(server_env) # 登记 pbl_runtime_conn_factory 等
load_world_sync() # 仅登记 CRUD + 表注册 + 契约
# 或宿主显式注入连接工厂(推荐,模块内部因此完全不需要知道凭据来源):
load_world_sync(server_env=env, conn_factory=my_factory)
# 业务侧写入
from world_sync import write_event_with_state
@ -21,64 +33,152 @@ M11b-2 增量:单事务内「事件 + 实体状态」原子落库。
``conn_factory`` 解析优先级 :mod:`world_sync.pbl_runtime_tx_env`
显式注入 ServerEnv 登记 环境变量 ``PBL_RUNTIME_DB_URL`` 模块自有 ``conf/db.json``
四级全空则 fail-closed :class:`ConnFactoryNotConfigured`
四级全空则 fail-closed :class:`ConnFactoryNotConfigured`别名 :class:`ConnectionUnavailable`
"""
# --- ② 导出实现层init.py为唯一真源本文件不重复定义任何函数 ---
from .init import (
# 挂载入口(唯一实现)
load_world_sync,
# 既有 world_sync CRUD
list_world_syncs,
get_world_sync,
create_world_sync,
update_world_sync,
delete_world_sync,
execute_world_sync,
# M11b-1 表注册(只读元数据)
MODULE_TABLES,
module_tables,
get_table_schema,
load_table_model,
pbl_runtime_event_schema,
pbl_runtime_event_partitions,
# M11b-2 运行时写入契约(配置 / 预演 / 诊断)
configure_runtime_writer,
get_runtime_writer,
build_runtime_write_plan,
runtime_write_percentile,
runtime_write_dialect_name,
runtime_write_ddl,
# 模块元信息
MODULE_NAME,
MODULE_VERSION,
SERVER_ENV_CONN_FACTORY_KEYS,
# 宿主基础包可用性(自检用,不建连)
HOST_DEPS_AVAILABLE,
HOST_DEPS_ERROR,
HostDepsNotAvailable,
)
# --- M11b-2 契约函数与异常族真源pbl_runtime_tx* 系列init.py 亦登记同名) ---
from .pbl_runtime_errors import (
ConcurrentStateConflict,
ConnFactoryNotConfigured,
ConnectionUnavailable,
EventWriteError,
InvalidWriteRequest,
PblRuntimeError,
RuntimeWriteError,
StateWriteError,
TransactionAborted,
TxAbortError,
TxConfigError,
)
from .pbl_runtime_sql import (
DIALECT_MYSQL,
DIALECT_SQLITE,
EVENT_TABLE,
STATE_TABLE,
ddl_for,
detect_dialect,
percentile,
)
from .pbl_runtime_tx import (
DEFAULT_EVENT_TYPE,
DEFAULT_SOURCE,
EVENT_REQUIRED,
STATE_KEY_COLUMNS,
new_event_id,
read_entity_state as _tx_read_entity_state,
new_event_uid,
utc_now_text,
write_event_with_state as _tx_write_event_with_state,
)
from .pbl_runtime_tx_env import (
SERVER_ENV_KEYS,
build_mysql_conn_factory,
build_sqlite_conn_factory,
load_db_config,
module_conf_dir,
resolve_conn_factory,
)
from .pbl_runtime_tx_sqlor import (
ENV_NAME_KEYS,
POOL_KEYS,
build_conn_factory_from_pools,
host_conn_factory,
read_entity_state,
write_event_with_state,
)
MODULE_NAME = "world_sync"
MODULE_VERSION = "0.2.0-m11b2"
#: 宿主 ServerEnv 上登记的 conn_factory 键pbl_runtime_tx_env.SERVER_ENV_KEYS 与此一致)
SERVER_ENV_CONN_FACTORY_KEYS = ("pbl_runtime_conn_factory", "world_sync_conn_factory")
#: 契约解析优先级键(与 pbl_runtime_tx_env.SERVER_ENV_KEYS 一致,单一来源)
assert tuple(SERVER_ENV_CONN_FACTORY_KEYS) == tuple(SERVER_ENV_KEYS), \
"world_sync: SERVER_ENV_CONN_FACTORY_KEYS 与 pbl_runtime_tx_env.SERVER_ENV_KEYS 分叉"
__all__ = [
# 写入 / 读取M11b-2 核心)
"write_event_with_state",
"read_entity_state",
# 接线
# 挂载入口(唯一实现位于 init.py
"load_world_sync",
"MODULE_NAME",
"MODULE_VERSION",
"MODULE_TABLES",
"SERVER_ENV_CONN_FACTORY_KEYS",
"SERVER_ENV_KEYS",
# 既有 CRUD
"list_world_syncs",
"get_world_sync",
"create_world_sync",
"update_world_sync",
"delete_world_sync",
"execute_world_sync",
# M11b-1 表注册(只读)
"module_tables",
"get_table_schema",
"load_table_model",
"pbl_runtime_event_schema",
"pbl_runtime_event_partitions",
# M11b-2 写入 / 读取契约(核心)
"write_event_with_state",
"read_entity_state",
"configure_runtime_writer",
"get_runtime_writer",
"build_runtime_write_plan",
"runtime_write_percentile",
"runtime_write_dialect_name",
"runtime_write_ddl",
# 工厂与方言
"resolve_conn_factory",
"build_sqlite_conn_factory",
"build_mysql_conn_factory",
"build_conn_factory_from_pools",
"host_conn_factory",
"load_db_config",
"module_conf_dir",
"detect_dialect",
"ddl_for",
"percentile",
"EVENT_TABLE",
"STATE_TABLE",
"DIALECT_MYSQL",
"DIALECT_SQLITE",
"DEFAULT_EVENT_TYPE",
"DEFAULT_SOURCE",
"EVENT_REQUIRED",
"STATE_KEY_COLUMNS",
"POOL_KEYS",
"ENV_NAME_KEYS",
"new_event_id",
"new_event_uid",
"utc_now_text",
# 异常族
# 异常族(含 M11b-2a 语义别名,同一对象)
"PblRuntimeError",
"TxConfigError",
"ConnFactoryNotConfigured",
@ -86,34 +186,12 @@ __all__ = [
"StateWriteError",
"ConcurrentStateConflict",
"TxAbortError",
"RuntimeWriteError",
"InvalidWriteRequest",
"ConnectionUnavailable",
"TransactionAborted",
# 宿主基础包可用性诊断
"HOST_DEPS_AVAILABLE",
"HOST_DEPS_ERROR",
"HostDepsNotAvailable",
]
def load_world_sync(server_env=None, conn_factory=None):
"""挂载 world_sync向 ServerEnv 登记本模块能力与可选conn_factory。
幂等重复调用只覆盖同名键不产生副作用返回本模块 ``__all__`` 便于宿主自检
:param server_env: 宿主 ServerEnv 实例None 时尝试取全局单例
:param conn_factory: 宿主连接池工厂提供则登记到 ``pbl_runtime_conn_factory``
使模块内部无需知道任何部署环境凭据来源fail-closed 而非自行找配置文件
"""
if server_env is None:
try:
from appPublic.serverEnv import ServerEnv
server_env = ServerEnv()
except Exception:
server_env = None
if server_env is not None:
if conn_factory is not None:
setattr(server_env, SERVER_ENV_CONN_FACTORY_KEYS[0], conn_factory)
setattr(server_env, "world_sync_module", MODULE_NAME)
setattr(server_env, "world_sync_version", MODULE_VERSION)
setter = getattr(server_env, "set", None)
if callable(setter):
try:
setter("world_sync_version", MODULE_VERSION)
except Exception:
pass
return list(__all__)

View File

@ -23,10 +23,79 @@ M11b-2a 迁移修正(响应 QC #1本文件此前导入的
import json
import os
from appPublic.uniqueID import getID
from appPublic.timeUtils import curDateString, timestampstr
from sqlor.dbpools import DBPools
from ahserver.serverenv import ServerEnv
# 宿主基础包appPublic / sqlor / ahserverM11b-2a 起改为「容错导入 + 显式记录」——
# 目的是让「三处接线」自检import 本包 + 调 load_world_sync())不依赖宿主部署环境,
# 而不是吞错误:基础包缺失时记入 HOST_DEPS_ERROR任何真正访问数据库的函数在调用点
# 通过 _require_host_deps() fail-fast 抛出带原因的 RuntimeError不静默返回 None
HOST_DEPS_ERROR = None
try:
from appPublic.uniqueID import getID
from appPublic.timeUtils import curDateString, timestampstr
from sqlor.dbpools import DBPools
from ahserver.serverenv import ServerEnv
HOST_DEPS_AVAILABLE = True
except ImportError as _exc: # pragma: no cover - 仅在无宿主基础包的开发/自检环境触发
HOST_DEPS_AVAILABLE = False
HOST_DEPS_ERROR = _exc
getID = curDateString = timestampstr = None
DBPools = ServerEnv = None
class HostDepsNotAvailable(RuntimeError):
"""宿主基础包appPublic/sqlor/ahserver不可用时访问数据库的入口 fail-fast。"""
def __init__(self, op):
super().__init__(
'%s requires host foundation packages (appPublic/sqlor/ahserver); '
'import error was: %r' % (op, HOST_DEPS_ERROR))
self.op = op
def _require_host_deps(op):
if not HOST_DEPS_AVAILABLE:
raise HostDepsNotAvailable(op)
return True
class _StandaloneEnv(object):
"""无宿主 ServerEnv 时的登记载体(仅供接线自检,不建连、不接库)。"""
def __init__(self):
self._kv = {}
def __setattr__(self, k, v):
object.__setattr__(self, k, v)
def set(self, k, v):
self._kv[k] = v
def get(self, k, default=None):
return getattr(self, k, self._kv.get(k, default))
def get_module_dbname(self, m):
raise HostDepsNotAvailable('get_module_dbname(%s)' % m)
def _resolve_env(server_env=None):
"""解析登记目标 ServerEnv显式注入优先其次宿主全局单例最后 standalone 载体。"""
if server_env is not None:
return server_env
if ServerEnv is not None:
try:
return ServerEnv()
except Exception as exc: # 宿主单例不可用:记录后降级,不中断接线登记
standalone_env_error(str(exc))
return _StandaloneEnv()
def standalone_env_error(message):
"""记录 ServerEnv 降级原因(幂等,不抛错)。"""
global STANDALONE_ENV_NOTICES
STANDALONE_ENV_NOTICES = list(STANDALONE_ENV_NOTICES) + [message]
return message
STANDALONE_ENV_NOTICES = []
# --- M11b-2: 单事务写入(事件 + 实体状态)注册接线(真源实际存在的名字) ---
from .pbl_runtime_errors import (
@ -38,6 +107,7 @@ from .pbl_runtime_errors import (
PblRuntimeError,
RuntimeWriteError,
StateWriteError,
TransactionAborted,
TxAbortError,
TxConfigError,
)
@ -79,7 +149,14 @@ from .pbl_runtime_tx_sqlor import (
)
MODULE_NAME = 'world_sync'
MODULE_VERSION = '0.2.0-m11b2a'
#: 宿主 ServerEnv 上登记 conn_factory 的键(与 pbl_runtime_tx_env.SERVER_ENV_KEYS 一致)
SERVER_ENV_CONN_FACTORY_KEYS = SERVER_ENV_KEYS
def _get_dbname():
_require_host_deps('_get_dbname')
return ServerEnv().get_module_dbname('world_sync')
@ -91,12 +168,14 @@ def _clean(ns):
async def list_world_syncs(params=None):
_require_host_deps('list_world_syncs')
db = DBPools()
async with db.sqlorContext(_get_dbname()) as sor:
return await sor.sqlExe("select id, world_id, sync_type, source, status, result_json, sync_date, created_at from world_sync order by created_at desc", {})
async def get_world_sync(ns):
_require_host_deps('get_world_sync')
rec_id = (ns or {}).get('id')
if not rec_id:
return None
@ -107,6 +186,7 @@ async def get_world_sync(ns):
async def create_world_sync(ns):
_require_host_deps('create_world_sync')
ns = _clean(dict(ns))
ns['id'] = ns.get('id') or getID()
if not ns.get('sync_date'):
@ -121,6 +201,7 @@ async def create_world_sync(ns):
async def update_world_sync(ns):
_require_host_deps('update_world_sync')
ns = _clean(dict(ns))
rec_id = ns.get('id')
if not rec_id:
@ -132,6 +213,7 @@ async def update_world_sync(ns):
async def delete_world_sync(ns):
_require_host_deps('delete_world_sync')
rec_id = (ns or {}).get('id')
if not rec_id:
return {'success': False, 'message': '缺少 id'}
@ -142,6 +224,7 @@ async def delete_world_sync(ns):
async def execute_world_sync(ns):
_require_host_deps('execute_world_sync')
ns = ns or {}
world_id = ns.get('world_id', '')
sync_type = ns.get('sync_type', '0')
@ -226,6 +309,7 @@ async def pbl_runtime_event_partitions(params=None):
" where table_schema=${dbname}$ and table_name=${tbl}$ "
" and partition_name is not null "
" order by partition_ordinal_position")
_require_host_deps('pbl_runtime_event_partitions')
db = DBPools()
async with db.sqlorContext(_get_dbname()) as sor:
return await sor.sqlExe(sql, {'dbname': _get_dbname(), 'tbl': PBL_RUNTIME_EVENT_TABLE})
@ -331,12 +415,28 @@ def runtime_write_ddl(ns):
return {'success': False, 'message': str(exc)}
def load_world_sync():
"""挂载 world_syncCRUD + M11b-1 表注册 + M11b-2 单事务写入契约
def load_world_sync(server_env=None, conn_factory=None):
"""挂载 world_syncCRUD + M11b-1 表注册 + M11b-2 单事务写入契约(唯一真源)
三处接线之 init.py env.xxx = xxx导入失败一律原样抛出不静默降级
三处接线之 init.py env.xxx = xxx本函数同时是 __init__.py 导出
根包 ``world_sync/__init__.py`` 转发的**唯一** load_world_sync 实现QC #2
不得存在第二份定义模块级符号 ``TransactionAborted``导入失败一律原样
抛出不静默降级
:param server_env: 宿主 ServerEnv 实例None 时取全局单例单例不可用时用
:class:`_StandaloneEnv` 载体完成登记仅接线自检不建连
:param conn_factory: 宿主连接池工厂可调用/DB URL提供则登记到
``pbl_runtime_conn_factory``契约解析优先级第 2
:return: 登记完成的 env
"""
env = ServerEnv()
env = _resolve_env(server_env)
if conn_factory is not None:
setattr(env, SERVER_ENV_KEYS[0], conn_factory)
env.world_sync_module = MODULE_NAME
env.world_sync_version = MODULE_VERSION
env.world_sync_host_deps_available = HOST_DEPS_AVAILABLE
if not HOST_DEPS_AVAILABLE:
env.world_sync_host_deps_error = repr(HOST_DEPS_ERROR)
env.list_world_syncs = list_world_syncs
env.get_world_sync = get_world_sync
env.create_world_sync = create_world_sync
@ -371,6 +471,8 @@ def load_world_sync():
env.StateWriteError = StateWriteError
env.ConcurrentStateConflict = ConcurrentStateConflict
env.TransactionAborted = TransactionAborted
env.RuntimeTxAbortError = TxAbortError
env.new_runtime_event_id = new_event_id
# 表名/方言常量与 conn_factory 解析优先级说明
env.pbl_runtime_event_table = EVENT_TABLE
env.pbl_runtime_state_table = STATE_TABLE