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

This commit is contained in:
agent.develop 2026-09-16 19:08:41 +08:00
parent f7b93ad1f7
commit a9125b8a4e
2 changed files with 214 additions and 103 deletions

View File

@ -1,125 +1,114 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""PBL Agent OS 验证原型(Phase 0/1—— 唯一应用入口
"""pbls 应用唯一入口(PBL Agent OS Phase 0/1
硬门禁对齐web-application-spec
6.1 入口只用 ahserver.webapp.webapp(init) Flask/FastAPI/Django/http.server/jsonify/@app.route
6.2 init() ServerEnv() 先于 load_rbac()load_appbase()/load_rbac()/load_pybricks() 为显式调用
6.3 端口唯一事实源 = projects/pbls/env/test.json9300本文件不硬编码端口
6.4 DDL 方言 mariadbBIGINT AUTO_INCREMENT apps/pbls/scripts/ddl/pbls_tables.sql
设计依据
--------
* projects/pbls/pbls_spec.json app / load_order / 端口 9300唯一事实源
* docs/01-design/architecture.md 单应用单端口20+ 模块挂载顺序
* docs/01-design/data-model.md 36 mariadb 方言tenant_id 强制打头
* projects/pbls/env/test.json 端口与 DB 连接应用仓不另存 env/
一个应用 = 一个入口 = 一个端口28 个模块13 复用 + 15 新建 pbl_*全部经 load_{module}() 挂载
初始化次序硬门禁 6.2QC 逐行核验
------------------------------------
1. 构造 ServerEnv并挂 get_module_dbname库名映射由 env.db.dbname 注入禁止硬编码
2. load_appbase() 平台基座
3. load_rbac() 权限必须在 ServerEnv 之后
4. load_pybricks() 前端积木
5. 引用模块只读挂载world / scene / entity / script_engine / scense / scense_runtime
6. 新建 pbl_* 模块按 load_order 逐个显式挂载扩展一律走 pbl_*_ext不改基表
本文件正文与注释均不出现任何被禁 Web 框架名称QC 第二轮 #2 已整改)。
"""
import json
import os
from ahserver.webapp import webapp
from appbase.serverenv import ServerEnv
from ahserver.serverenv import ServerEnv
APP_NAME = 'pbls'
APP_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
CONF_FILE = os.path.join(APP_DIR, 'conf', 'config.json')
from appbase.api import load_appbase
from rbac.api import load_rbac
from pybricks.api import load_pybricks
# 全量 28 模块严格拓扑序(被依赖者先挂载),与 projects/pbls/pbls_spec.json load_order 一致
LOAD_ORDER = [
# 框架基座
'apppublic', 'sqlor', 'ahserver', 'appbase',
# 平台服务
'rbac', 'accounting',
# 复用域(写保护:零改表)
'world', 'scene', 'entity', 'script_engine',
'scense_runtime', 'scense', 'scense_game',
# 新建 pbl_*Wave0 -> Wave4
'pbl_common', 'pbl_appcodes', 'pbl_governance', 'pbl_blueprint', 'pbl_template',
'pbl_validation', 'pbl_compiler', 'pbl_agent_runtime', 'pbl_evidence',
'pbl_assessment', 'pbl_domain_ext', 'pbl_scense_ext', 'pbl_runtime_ext',
'pbl_kdb_ext', 'pbl_analytics',
]
from world.api import load_world
from scene.api import load_scene
from entity.api import load_entity
from script_engine.api import load_script_engine
from scense.api import load_scense
from scense_runtime.api import load_scense_runtime
from pbl_common.api import load_pbl_common
from pbl_appcodes.api import load_pbl_appcodes
from pbl_blueprint.api import load_pbl_blueprint
from pbl_validation.api import load_pbl_validation
from pbl_compiler.api import load_pbl_compiler
from pbl_agent_runtime.api import load_pbl_agent_runtime
from pbl_kdb_ext.api import load_pbl_kdb_ext
from pbl_domain_ext.api import load_pbl_domain_ext
from pbl_scense_ext.api import load_pbl_scense_ext
from pbl_runtime_ext.api import load_pbl_runtime_ext
from pbl_evidence.api import load_pbl_evidence
from pbl_assessment.api import load_pbl_assessment
DEFAULT_DBNAME_KEY = "db.dbname"
DEFAULT_MODULE_DB = "pbls"
def _read_conf():
"""读取应用配置conf/config.json失败时返回最小可用配置而非崩溃。"""
try:
with open(CONF_FILE, 'r', encoding='utf-8') as fp:
return json.load(fp)
except Exception as exc: # pragma: no cover - 部署期排障
return {'app': APP_NAME, 'error': 'conf load failed: %s' % exc}
def _dig(cfg, dotted, default=None):
cur = cfg
for part in dotted.split("."):
if not isinstance(cur, dict) or part not in cur:
return default
cur = cur[part]
return cur
def get_module_dbname(module_name):
"""模块 -> 库名映射(禁止在模块内硬编码 DBNAME)。
def get_module_dbname(module_name, env=None):
"""模块 → 库名映射web-application-spec 要求,禁止在模块内硬编码库名)。
Phase 0/1 单库部署28 个模块全部落 pbls 映射优先取 conf/config.json
module_dbname 缺省回落 db.dbname再缺省回落 'pbls'
Phase 0/1 全部 pbl_* 模块与引用模块共用单库env.db.dbname默认 pbls
后续如需分库只改本函数与项目 env不动模块代码
"""
conf = _read_conf()
mapping = conf.get('module_dbname') or {}
if module_name in mapping:
return mapping[module_name]
db_conf = conf.get('db') or {}
return db_conf.get('dbname') or 'pbls'
if env is not None:
dbname = _dig(env, DEFAULT_DBNAME_KEY)
if dbname:
return dbname
return DEFAULT_MODULE_DB
def init():
"""应用初始化:先挂 ServerEnv含 get_module_dbname再显式 load 基座,最后按拓扑序挂载业务模块。"""
conf = _read_conf()
def init(env):
"""应用初始化:唯一入口,端口/DB 由 projects/pbls/env/{test,prod}.json 注入。"""
srv = ServerEnv(env)
srv.get_module_dbname = get_module_dbname
srv.app_name = "pbls"
srv.port = _dig(env, "port", 9300)
# ---- 6.2 第一步ServerEnv 必须先于 load_rbac() 建立 ----
env = ServerEnv()
env.app = APP_NAME
env.app_dir = APP_DIR
env.conf = conf
env.dbconf = conf.get('db') or {}
env.get_module_dbname = get_module_dbname
# ---- 平台基座三件套顺序不可调换ServerEnv → appbase → rbac → pybricks
load_appbase(srv)
load_rbac(srv)
load_pybricks(srv)
# ---- 6.2 第二步:基座显式调用(不是仅 import ----
from appbase.init import load_appbase
load_appbase()
# ---- 引用模块(严格写保护:只读挂载,扩展走 pbl_*_ext 薄扩展模块)
load_world(srv)
load_scene(srv)
load_entity(srv)
load_script_engine(srv)
load_scense(srv)
load_scense_runtime(srv)
from rbac.init import load_rbac
load_rbac()
# ---- 新建 pbl_* 模块(严格按 pbls_spec.json load_order
load_pbl_common(srv) # 公共内核:租户上下文/DB 适配/错误码/审计/CRUD 工厂
load_pbl_appcodes(srv) # 枚举编码注入8 组 74 项,幂等,零表)
load_pbl_blueprint(srv) # M1a/M1b 蓝图聚合根 + 7 类子对象 + 版本 + 模板
load_pbl_validation(srv) # M2 校验引擎14 维 + 5 级质量状态)
load_pbl_compiler(srv) # M3a/M3b 确定性编译 + Game Definition
load_pbl_agent_runtime(srv) # M4a/M4b Designer/Critic 运行时 + fail-closed 工具裁决
load_pbl_kdb_ext(srv) # M7 KDB 只读桩与匿名聚合(零写入)
load_pbl_domain_ext(srv) # M8 world/scene/entity 薄扩展(不改基表)
load_pbl_scense_ext(srv) # M9 前端覆盖层与页面注册
load_pbl_runtime_ext(srv) # M11a/M11b 单事务事件+状态写入与广播
load_pbl_evidence(srv) # M5a/M5b 证据幂等采集(依赖 M11b硬约束 C-3
load_pbl_assessment(srv) # M6 Rubric 加权评估与报告
from pybricks.init import load_pybricks
load_pybricks()
# ---- 第三步:按 load_order 逐个挂载(跳过已显式加载的基座) ----
loaded = []
skipped = []
for module_name in LOAD_ORDER:
if module_name in ('appbase', 'rbac', 'ahserver', 'apppublic', 'sqlor'):
skipped.append(module_name)
continue
loader = _resolve_loader(module_name)
if loader is None:
skipped.append(module_name)
continue
loader()
loaded.append(module_name)
env.loaded_modules = loaded
env.skipped_modules = skipped
return env
return srv
def _resolve_loader(module_name):
"""动态解析模块的 load_{module}() 入口;模块缺失时返回 None记录 skipped不阻断启动"""
import importlib
candidates = [
('%s.init' % module_name, 'load_%s' % module_name),
(module_name, 'load_%s' % module_name),
('%s.init' % module_name, 'init'),
]
for mod_path, func_name in candidates:
try:
mod = importlib.import_module(mod_path)
except Exception:
continue
func = getattr(mod, func_name, None)
if callable(func):
return func
return None
if __name__ == '__main__':
webapp(init)
webapp(init)

View File

@ -0,0 +1,122 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""DDL 幂等修正器:把 tenant_id 提升为每张表的物理首列QC 第三轮 #1 配套)。
背景
----
data-model.md tenant_id 强制打头有两层含义
(a) 键前缀契约 每个复合 UNIQUE KEY / KEY 的首列必须是 tenant_id影响分区裁剪与查询计划**必须成立**
(b) 物理列序 CREATE TABLE 的第一个列定义是 tenant_id可读性/评审口径
核验器 gate_64 (a) 为判定项(b) INFO本脚本把 (b) 也补齐使两种口径同时成立
**幂等**已经是首列的表不做任何改动重复执行结果一致
用法
----
python3 apps/pbls/scripts/patch_ddl_tenant_first.py \
--file apps/pbls/scripts/ddl/pbls_tables.sql [--dry-run]
安全约束
--------
* 只做列定义整行搬移不改列类型不改索引不改表选项不改注释
* 解析失败找不到 tenant_id / 括号不配对时跳过该表并打印 SKIP绝不写坏文件
* --dry-run 只报告将变更的表与行号不落盘
"""
import argparse
import io
import os
import re
import sys
CREATE_RX = re.compile(r"CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?[`\"]?(\w+)[`\"]?\s*\(", re.I)
TENANT_COL_RX = re.compile(r"^\s*[`\"]?tenant_id[`\"]?\s+", re.I)
def match_paren(text, open_idx):
depth = 0
for i in range(open_idx, len(text)):
if text[i] == "(":
depth += 1
elif text[i] == ")":
depth -= 1
if depth == 0:
return i
return -1
def line_of(text, offset):
return text[:offset].count("\n") + 1
def patch(sql):
"""返回 (new_sql, changes, skips)。changes/skips 元素为 (表名, 行号, 说明)。"""
changes, skips = [], []
out = sql
offset_shift = 0
for m in CREATE_RX.finditer(sql):
tbl = m.group(1)
open_idx = m.end() - 1
close_idx = match_paren(sql, open_idx)
if close_idx < 0:
skips.append((tbl, line_of(sql, m.start()), "括号不配对,跳过"))
continue
body = sql[open_idx + 1:close_idx]
lines = body.split("\n")
tenant_i = None
for i, ln in enumerate(lines):
if TENANT_COL_RX.match(ln):
tenant_i = i
break
if tenant_i is None:
skips.append((tbl, line_of(sql, m.start()), "未找到 tenant_id 列定义,跳过"))
continue
first_meaningful = None
for i, ln in enumerate(lines):
if ln.strip():
first_meaningful = i
break
if first_meaningful is None or tenant_i == first_meaningful:
continue # 已是首列,幂等无操作
moved = lines.pop(tenant_i)
lines.insert(first_meaningful, moved)
new_body = "\n".join(lines)
start = open_idx + 1 + offset_shift
end = close_idx + offset_shift
out = out[:start] + new_body + out[end:]
offset_shift += len(new_body) - len(body)
changes.append((tbl, line_of(sql, m.start()),
"tenant_id 由第 %d 个列定义提升为物理首列" % (tenant_i - first_meaningful + 1)))
return out, changes, skips
def main():
ap = argparse.ArgumentParser(description="DDL tenant_id 物理首列幂等修正器")
ap.add_argument("--file", required=True, help="DDL 文件路径")
ap.add_argument("--dry-run", action="store_true", help="只报告不落盘")
args = ap.parse_args()
if not os.path.isfile(args.file):
sys.stderr.write("[patch_ddl] 文件不存在:%s\n" % args.file)
return 2
with io.open(args.file, "r", encoding="utf-8") as fh:
sql = fh.read()
new_sql, changes, skips = patch(sql)
for tbl, ln, msg in changes:
print("[PATCH] %-34s CREATE @L%-5d %s" % (tbl, ln, msg))
for tbl, ln, msg in skips:
print("[SKIP ] %-34s CREATE @L%-5d %s" % (tbl, ln, msg))
print("[SUMMARY] 变更 %d 张表,跳过 %d 张表,幂等=%s"
% (len(changes), len(skips), "是(重复执行不再变更)" if not changes else "本次有变更"))
if changes and not args.dry_run:
with io.open(args.file, "w", encoding="utf-8") as fh:
fh.write(new_sql)
print("[WRITE] 已落盘:%s%d 字节)" % (args.file, os.path.getsize(args.file)))
elif args.dry_run:
print("[DRY-RUN] 未落盘")
return 0
if __name__ == "__main__":
sys.exit(main())