deliver: 交付收口(引擎代为提交)
This commit is contained in:
parent
f7b93ad1f7
commit
a9125b8a4e
195
app/pbls.py
195
app/pbls.py
@ -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.json(9300),本文件不硬编码端口
|
||||
6.4 DDL 方言 mariadb(BIGINT 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.2,QC 逐行核验)
|
||||
------------------------------------
|
||||
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)
|
||||
|
||||
122
scripts/patch_ddl_tenant_first.py
Normal file
122
scripts/patch_ddl_tenant_first.py
Normal 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())
|
||||
Loading…
x
Reference in New Issue
Block a user