feat(pbls): 初始提交 - pbls 项目模块代码入库

This commit is contained in:
pbls-bot 2026-09-15 17:39:29 +08:00 committed by agent.develop
parent 6cc922e46a
commit a494bd23e6
10 changed files with 2580 additions and 0 deletions

12
.gitignore vendored Normal file
View File

@ -0,0 +1,12 @@
__pycache__/
*.pyc
*.pyo
.pytest_cache/
.coverage
.DS_Store
*.log
node_modules/
dist/
build/
.venv/
venv/

111
app/pbls.py Normal file
View File

@ -0,0 +1,111 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""pbls —— PBL Agent OS 应用唯一入口(端口 9300见 projects/pbls/env/test.json:port
框架铁律web-application-spec
- 入口必须 `from ahserver.webapp import webapp` + 末尾 `webapp(init)`
全项目零其它 Web 框架零自定义路由装饰器零手工 JSON 响应包装verify_delivery.py 扫描零命中
- 加载顺序固定`ServerEnv()` 基础模块appbase/rbac/pybricks 业务模块
基础模块必须**显式调用 loader**load_appbase/load_rbac/load_pybricks
importlib.import_module 就标 loaded 是反模式登录/权限链路必崩
- 库名由应用统一决定get_module_dbname(m) conf/config.json module_dbmap
模块侧禁止硬编码 DBNAME
- 端口唯一来源 = projects/pbls/env/{test,prod}.json port应用仓库不设 env/ 目录
挂载规模对账QC #1320 个 load_* 调用 = 3 平台基础模块appbase/rbac/pybricks
+ 5 复用基座模块world/scene/entity/script_engine/scense_game
+ 12 pbl_* 模块见下 MODULE_LOAD_ORDER编号 M1~M12 modules/*.md 一一对应
"""
import json
import os
from ahserver.webapp import webapp
from ahserver.serverenv import ServerEnv
import bricks_for_python # registers bui processor
from bricks_for_python.init import load_pybricks # registers UiWindow etc.
from appbase.init import load_appbase
from rbac.init import load_rbac
CONF = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
'conf', 'config.json')
# 12 个 pbl_* 业务模块(编号 ↔ 目录M10=pbl_appcodes 零表、M12=pbl_common 内核)
MODULE_LOAD_ORDER = [
('pbl', 'pbl_common'), # M12 公共内核(必须最先,提供租户/DB 出口)
('pbl', 'pbl_appcodes'), # M10 枚举 6+2 组幂等注入
('pbl', 'pbl_blueprint'), # M1 蓝图聚合根 + 子对象 + 版本 + 模板
('pbl', 'pbl_validation'), # M2 14 维校验 + 5 级质量状态
('pbl', 'pbl_compiler'), # M3 确定性编译 + Game Definition + Capability
('pbl', 'pbl_agent_runtime'), # M4 Designer/Critic + fail-closed 工具裁决
('pbl', 'pbl_domain_ext'), # M8 world/scene/entity 薄扩展(不改基表)
('pbl', 'pbl_runtime_ext'), # M11 单事务事件+状态写入与广播
('pbl', 'pbl_evidence'), # M5 产出物 + 证据幂等采集
('pbl', 'pbl_assessment'), # M6 Rubric 加权评估与报告
('pbl', 'pbl_kdb_ext'), # M7 KDB 只读桩 + 匿名聚合
('pbl', 'pbl_scense_ext'), # M9 前端覆盖层上下文 + 页面注册
]
# 复用基座模块(只读引用 + 薄扩展,不改基表;缺失时降级并明确回报,不静默假成功)
BASE_MODULE_LOADERS = ['world', 'scene', 'entity', 'script_engine', 'scense_game']
def _conf():
with open(CONF, encoding='utf-8') as f:
return json.load(f)
def get_module_dbname(m):
"""模块 → 库名映射(来自 conf/config.json:module_dbmap禁止写死单个库名"""
conf = _conf()
mapping = conf.get('module_dbmap') or {}
if m in mapping:
return mapping[m]
return mapping.get('*') or conf.get('default_dbname') or 'pbl'
def password_encode(s):
if s is None:
return ''
from ahserver.globalEnv import password_encode as _orig
return _orig(s)
def init():
env = ServerEnv() # ← MUST be before load_rbac
env.get_module_dbname = get_module_dbname
env.password_encode = password_encode
env.pbl_default_dbname = get_module_dbname('pbl')
env.pbl_default_tenant = _conf().get('default_tenant') or 'default'
# ── 基础模块:显式调用 loader反模式禁令不得只 import 就标 loaded──
load_appbase()
load_rbac()
load_pybricks()
# ── 复用基座模块world/scene/entity/script_engine/scense_game──
missing = []
for name in BASE_MODULE_LOADERS:
try:
pkg = __import__('importlib').import_module('%s.init' % name)
loader = getattr(pkg, 'load_%s' % name, None)
if callable(loader):
loader()
else:
missing.append(name) # 无 loader 即视为未挂载,不谎报 loaded
except ImportError:
missing.append(name)
# ── 12 个 pbl_* 业务模块(按 Wave 依赖顺序)──
loaded = []
for _db, name in MODULE_LOAD_ORDER:
pkg = __import__('importlib').import_module('%s.init' % name)
loader = getattr(pkg, 'load_%s' % name) # 缺 loader 直接抛错fail-fast
loader()
loaded.append(name)
env.pbl_modules_loaded = loaded
env.pbl_base_missing = missing
print('[pbls] modules loaded: %d pbl + base missing=%s' % (len(loaded), missing or 'none'))
return {'loaded': len(loaded) + 3, 'base_missing': missing}
if __name__ == '__main__':
webapp(init)

68
build.sh Normal file
View File

@ -0,0 +1,68 @@
#!/bin/bash
# pbls 一键部署web-application-spec 8 步;端口 9300 来源 projects/pbls/env/test.json
set -euo pipefail
cdir=$(cd "$(dirname "$0")" && pwd)
APP=pbls
PORT=${PORT:-9300}
DB_HOST=127.0.0.1; DB_PORT=3306; DB_USER=test; DB_PASS=test123; DB_NAME=pbl
ENV_JSON="$cdir/../../projects/pbls/env/test.json"
[ -f "$ENV_JSON" ] && PORT=$(python3 -c "import json;print(json.load(open('$ENV_JSON'))['port'])")
echo "[build] app=$APP port=$PORT (env source: $ENV_JSON)"
mkdir -p "$cdir/pkgs" "$cdir/logs" "$cdir/files"
# 1) 基础包 + 依赖
pip install -q -r "$cdir/requirements.txt" || echo "[warn] pip requirements 需离线源,跳过"
# 2) 模块 clone + 安装(基础模块 + 复用基座 + 12 个 pbl_*
MODULES="world scene entity script_engine scense scense_game scense_runtime \
pbl_common pbl_appcodes pbl_blueprint pbl_validation pbl_compiler pbl_agent_runtime \
pbl_evidence pbl_assessment pbl_kdb_ext pbl_domain_ext pbl_scense_ext pbl_runtime_ext"
for m in $MODULES; do
src="$cdir/../../modules/$m"
[ -d "$src" ] || { echo "[skip] module $m 不存在"; continue; }
[ -d "$cdir/pkgs/$m" ] || cp -r "$src" "$cdir/pkgs/$m"
if [ -f "$cdir/pkgs/$m/pyproject.toml" ]; then
pip install -q -e "$cdir/pkgs/$m" || echo "[warn] pip install -e $m 失败,回退 PYTHONPATH"
fi
export PYTHONPATH="$cdir/pkgs:$PYTHONPATH"
# 3) wwwroot 软链(非 cp保持同步
ln -sfn "$cdir/pkgs/$m/wwwroot" "$cdir/wwwroot/$m"
# 5) 建表models/*.json → DDLjson2ddl与 apps/pbls/scripts/ddl 同源
if [ -d "$cdir/pkgs/$m/models" ]; then
( cd "$cdir/pkgs/$m/models" && json2ddl mysql . > mysql.ddl.sql 2>/dev/null || true )
[ -s "$cdir/pkgs/$m/models/mysql.ddl.sql" ] && \
mysql -h "$DB_HOST" -P "$DB_PORT" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" \
< "$cdir/pkgs/$m/models/mysql.ddl.sql" || echo "[warn] DDL 执行跳过($m"
fi
# 6) CRUD 生成json/*.json → .dspy + .ui
if [ -d "$cdir/pkgs/$m/json" ]; then
( cd "$cdir/pkgs/$m/json" && xls2ui -m ../models -o ../wwwroot "$m" *.json 2>/dev/null || true )
fi
# 7) 初始化数据appcodes 等种子)
[ -f "$cdir/pkgs/$m/init/data.json" ] && echo "[info] $m 有 init/data.json由 load_$m() 幂等注入"
# 8) RBAC 路径注册
[ -f "$cdir/pkgs/$m/scripts/load_path.py" ] && \
python3 "$cdir/pkgs/$m/scripts/load_path.py" || echo "[warn] $m rbac 注册跳过"
done
# 4) i18n 合并
python3 "$cdir/scripts/merge_i18n.py" 2>/dev/null || echo "[info] i18n 合并跳过"
# 兜底建表36 表MariaDB 方言,幂等 CREATE TABLE IF NOT EXISTS
bash "$cdir/scripts/apply_ddl.sh" || echo "[warn] apply_ddl 跳过(离线/无库)"
# bricks 前端
[ -d "$cdir/pkgs/bricks/dist" ] && ln -sfn "$cdir/pkgs/bricks/dist" "$cdir/wwwroot/bricks"
# 启动/停止脚本
cat > "$cdir/start.sh" <<EOF
#!/bin/bash
cd "$cdir" && PYTHONPATH="$cdir/pkgs:\$PYTHONPATH" python3 app/$APP.py >> logs/$APP.out 2>&1 &
echo \$! > $APP.pid
EOF
cat > "$cdir/stop.sh" <<EOF
#!/bin/bash
cd "$cdir" && [ -f $APP.pid ] && kill \$(cat $APP.pid) && rm -f $APP.pid
EOF
chmod +x "$cdir/start.sh" "$cdir/stop.sh"
echo "[build] done. start: ./start.sh port: $PORT"

69
conf/config.json Normal file
View File

@ -0,0 +1,69 @@
{
"appname": "pbls",
"port": 9300,
"port_source": "projects/pbls/env/test.json:port端口唯一来源应用不设 env/ 目录)",
"default_dbname": "pbl",
"default_tenant": "default",
"module_dbmap": {
"*": "pbl",
"pbl_common": "pbl",
"pbl_appcodes": "pbl",
"pbl_blueprint": "pbl",
"pbl_validation": "pbl",
"pbl_compiler": "pbl",
"pbl_agent_runtime": "pbl",
"pbl_evidence": "pbl",
"pbl_assessment": "pbl",
"pbl_kdb_ext": "pbl",
"pbl_domain_ext": "pbl",
"pbl_scense_ext": "pbl",
"pbl_runtime_ext": "pbl",
"world": "pbl",
"scene": "pbl",
"entity": "pbl",
"script_engine": "pbl",
"scense_game": "pbl",
"appbase": "pbl",
"rbac": "pbl"
},
"password_key": "pbls-aes-key-0123456789abcdef",
"logger": {
"name": "pbls",
"level": "INFO",
"file": "$[workdir]$/logs/pbls.log"
},
"filesroot": "$[workdir]$/files",
"databases": {
"pbl": {
"driver": "mysql",
"kwargs": {
"host": "127.0.0.1",
"port": 3306,
"unix_socket": null,
"user": "test",
"password": "test123",
"db": "pbl",
"charset": "utf8mb4"
},
"note": "本机 MySQL 走 socket 会失败(/run/mysqld/mysqld.sock 不存在),必须显式 TCP 127.0.0.1:3306 且 unix_socket=null"
}
},
"website": {
"port": 9300,
"root": "$[workdir]$/wwwroot",
"indexes": ["index.ui", "index.html"],
"processors": [
[".tmpl", "tmpl"],
[".ui", "bui"],
[".dspy", "dspy"],
[".wss", "ws"],
[".ws", "ws"]
],
"session": {
"secret": "pbls-session-secret-please-rotate",
"cookie_name": "pbls_session",
"max_age": 86400
},
"rbac": true
}
}

17
requirements.txt Normal file
View File

@ -0,0 +1,17 @@
# pbls 应用依赖(硬门禁 6.5 / QC #4
# ── 平台基础依赖Web 框架 + DB 抽象 + 基础模块,缺一不可)────────────────
ahserver # Web 框架(唯一入口 webapp(init) + BricksUIProcessor 渲染 .ui
bricks_for_python # bricks Python 侧load_pybricks 注册 UiWindow 等)
sqlor # DB 抽象层(只用 sor.C/U/D/R/I/sqlExe
apppublic # 公共工具db.sqlorContext / curDateString 等)
appbase # 应用基础params 表load_appbase 显式调用)
rbac # 登录与权限load_rbac 显式调用scripts/load_path.py 注册路径)
accounting # 平台基础(编码/序列,复用不改)
# ── 数据库驱动(缺 DB 驱动即连不上库)────────────────────────────────────
pymysql # MariaDB/MySQL Python 驱动TCP 127.0.0.1:3306
cryptography # pymysql caching_sha2_password 认证依赖
# ── 建表/CRUD 生成工具链build.sh 使用)─────────────────────────────────
xls2ddl # models/*.json → mysql.ddl.sqljson2ddl
# ── 复用基座模块(由 build.sh git clone + pip install -e 安装,此处仅登记)──
# world / scene / entity / script_engine / scense / scense_game / scense_runtime
# (机构 modules/ 仓库,非 PyPI 包,禁止在此写死版本号)

23
scripts/apply_ddl.sh Normal file
View File

@ -0,0 +1,23 @@
#!/usr/bin/env bash
# 幂等落库36 表 DDL + appcodes 注入MariaDB 10.x显式 TCP禁 unix_socket
set -euo pipefail
APP_HOME="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
DB_HOST="${PBLS_DB_HOST:-127.0.0.1}"
DB_PORT="${PBLS_DB_PORT:-3306}"
DB_USER="${PBLS_DB_USER:-}"
DB_NAME="${PBLS_DB_NAME:-pbls}"
echo "[ddl] target tcp://${DB_HOST}:${DB_PORT}/${DB_NAME} (unix_socket disabled)"
mysql --protocol=TCP -h "$DB_HOST" -P "$DB_PORT" -u "$DB_USER" ${PBLS_DB_PASSWORD:+-p"$PBLS_DB_PASSWORD"} \
-e "CREATE DATABASE IF NOT EXISTS \`$DB_NAME\` DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_general_ci;"
mysql --protocol=TCP -h "$DB_HOST" -P "$DB_PORT" -u "$DB_USER" ${PBLS_DB_PASSWORD:+-p"$PBLS_DB_PASSWORD"} \
--default-character-set=utf8mb4 "$DB_NAME" < "$APP_HOME/scripts/ddl/pbls_tables.sql"
TABLES=$(mysql --protocol=TCP -h "$DB_HOST" -P "$DB_PORT" -u "$DB_USER" ${PBLS_DB_PASSWORD:+-p"$PBLS_DB_PASSWORD"} -N -B -e \
"SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='$DB_NAME' AND table_name LIKE 'pbl_%';")
echo "[ddl] pbl_* tables in $DB_NAME = $TABLES (expect >= 36)"
# appcodes 6+2 组幂等注入
PYTHONPATH="$(dirname "$APP_HOME")/modules:$APP_HOME/lib:${PYTHONPATH:-}" \
python3 -c "import sys; sys.path.insert(0,'$(dirname "$APP_HOME")/modules'); import pbl_appcodes; print(pbl_appcodes.inject_all())"

668
scripts/ddl/pbls_tables.sql Normal file
View File

@ -0,0 +1,668 @@
-- pbls 36 表 DDLMariaDB 10.x 方言,由 scripts/gen_artifacts.py 从同一 spec 生成)
-- 硬约束tenant_id 打头 / id 为 BIGINT UNSIGNED AUTO_INCREMENT 主键;-- 不使用 外键约束 / 枚举列类型 / 时间戳列类型 / 位图列类型(枚举值走 appcodes 编码表,时间列走 DATETIME 由应用层写入)
-- 方言来源projects/pbls/env/test.json -> db.engine = mariadb禁 BIGSERIAL/SERIAL/nextval 等 PG 语法)
SET NAMES utf8mb4;
-- ===== pbl_blueprint (M1) =====
CREATE TABLE IF NOT EXISTS `pbl_blueprint` (
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT ID(),
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT ,
`code` VARCHAR(64) NOT NULL COMMENT "蓝图编码",
`title` VARCHAR(128) NOT NULL COMMENT "标题",
`subject` VARCHAR(64) NOT NULL COMMENT "学科",
`grade` VARCHAR(64) NOT NULL COMMENT "年级",
`description` TEXT NULL COMMENT "说明",
`quality_state` VARCHAR(64) NOT NULL COMMENT "质量状态 pbl_quality_state",
`generation_source` VARCHAR(64) NOT NULL COMMENT "生成来源 llm/template_fallback/manual",
`approval_status` VARCHAR(64) NOT NULL COMMENT "审批状态",
`visibility` VARCHAR(64) NOT NULL COMMENT "可见范围",
`content_json` LONGTEXT NULL COMMENT "蓝图正文(结构化模型,非散文)",
`version_no` INT NOT NULL DEFAULT 0 COMMENT "当前版本",
`compiler_version_id` BIGINT UNSIGNED NOT NULL COMMENT "编译版本",
`world_id` BIGINT UNSIGNED NOT NULL COMMENT "编译产物落 world 的溯源ID",
`created_by` VARCHAR(32) NOT NULL COMMENT "创建人",
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
PRIMARY KEY (`id`),
UNIQUE KEY `uk_bp_tenant_code` (`tenant_id`, `code`),
KEY `idx_bp_tenant_state` (`tenant_id`, `quality_state`),
KEY `idx_bp_tenant_creator` (`tenant_id`, `created_by`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="PBL 蓝图聚合根第8章 schema 落库)";
CREATE TABLE IF NOT EXISTS `pbl_blueprint_learning_goal` (
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT ID(),
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT ,
`blueprint_id` BIGINT UNSIGNED NOT NULL COMMENT "所属蓝图",
`obj_uid` VARCHAR(32) NOT NULL COMMENT "子对象UID",
`code` VARCHAR(64) NOT NULL COMMENT "目标编码",
`title` VARCHAR(128) NOT NULL COMMENT "标题",
`description` TEXT NULL COMMENT "描述",
`bloom_level` VARCHAR(64) NOT NULL COMMENT "布鲁姆层级",
`measurable` TINYINT(1) NOT NULL DEFAULT 0 COMMENT "是否可测",
`order_no` INT NOT NULL DEFAULT 0 COMMENT "排序",
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
PRIMARY KEY (`id`),
KEY `idx_blueprint_learning_goal_bp` (`tenant_id`, `blueprint_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="蓝图子对象 - 学习目标";
CREATE TABLE IF NOT EXISTS `pbl_blueprint_role` (
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT ID(),
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT ,
`blueprint_id` BIGINT UNSIGNED NOT NULL COMMENT "所属蓝图",
`obj_uid` VARCHAR(32) NOT NULL COMMENT "子对象UID",
`code` VARCHAR(64) NOT NULL COMMENT "角色编码",
`title` VARCHAR(128) NOT NULL COMMENT "角色名",
`persona_txt` TEXT NULL COMMENT "人设",
`capabilities_json` LONGTEXT NULL COMMENT "能力引用",
`headcount` INT NOT NULL DEFAULT 0 COMMENT "名额",
`order_no` INT NOT NULL DEFAULT 0 COMMENT "排序",
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
PRIMARY KEY (`id`),
KEY `idx_blueprint_role_bp` (`tenant_id`, `blueprint_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="蓝图子对象 - PBL 角色";
CREATE TABLE IF NOT EXISTS `pbl_blueprint_mission` (
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT ID(),
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT ,
`blueprint_id` BIGINT UNSIGNED NOT NULL COMMENT "所属蓝图",
`obj_uid` VARCHAR(32) NOT NULL COMMENT "子对象UID",
`code` VARCHAR(64) NOT NULL COMMENT "任务域编码",
`title` VARCHAR(128) NOT NULL COMMENT "驱动问题域",
`scenario_txt` TEXT NULL COMMENT "情境",
`unlock_condition_json` LONGTEXT NULL COMMENT "解锁条件",
`order_no` INT NOT NULL DEFAULT 0 COMMENT "排序",
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
PRIMARY KEY (`id`),
KEY `idx_blueprint_mission_bp` (`tenant_id`, `blueprint_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="蓝图子对象 - 驱动问题/Mission";
CREATE TABLE IF NOT EXISTS `pbl_blueprint_task` (
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT ID(),
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT ,
`blueprint_id` BIGINT UNSIGNED NOT NULL COMMENT "所属蓝图",
`obj_uid` VARCHAR(32) NOT NULL COMMENT "子对象UID",
`mission_id` BIGINT UNSIGNED NOT NULL COMMENT "所属Mission",
`code` VARCHAR(64) NOT NULL COMMENT "编码",
`title` VARCHAR(128) NOT NULL COMMENT "任务名",
`steps_json` LONGTEXT NULL COMMENT "步骤",
`difficulty` VARCHAR(64) NOT NULL COMMENT "难度",
`order_no` INT NOT NULL DEFAULT 0 COMMENT "排序",
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
PRIMARY KEY (`id`),
KEY `idx_blueprint_task_bp` (`tenant_id`, `blueprint_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="蓝图子对象 - 任务";
CREATE TABLE IF NOT EXISTS `pbl_blueprint_artifact_spec` (
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT ID(),
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT ,
`blueprint_id` BIGINT UNSIGNED NOT NULL COMMENT "所属蓝图",
`obj_uid` VARCHAR(32) NOT NULL COMMENT "子对象UID",
`code` VARCHAR(64) NOT NULL COMMENT "编码",
`title` VARCHAR(128) NOT NULL COMMENT "产出物名",
`artifact_type` VARCHAR(64) NOT NULL COMMENT "产出类型",
`required_fields_json` LONGTEXT NULL COMMENT "必填字段",
`order_no` INT NOT NULL DEFAULT 0 COMMENT "排序",
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
PRIMARY KEY (`id`),
KEY `idx_blueprint_artifact_spec_bp` (`tenant_id`, `blueprint_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="蓝图子对象 - 产出物规格";
CREATE TABLE IF NOT EXISTS `pbl_blueprint_evidence_spec` (
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT ID(),
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT ,
`blueprint_id` BIGINT UNSIGNED NOT NULL COMMENT "所属蓝图",
`obj_uid` VARCHAR(32) NOT NULL COMMENT "子对象UID",
`evidence_type` VARCHAR(64) NOT NULL COMMENT "证据类型",
`source` VARCHAR(64) NOT NULL COMMENT "来源",
`min_count` INT NOT NULL DEFAULT 0 COMMENT "最少条数",
`rubric_criterion_uid` VARCHAR(32) NOT NULL COMMENT "对齐的评分准则UID",
`order_no` INT NOT NULL DEFAULT 0 COMMENT "排序",
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
PRIMARY KEY (`id`),
KEY `idx_blueprint_evidence_spec_bp` (`tenant_id`, `blueprint_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="蓝图子对象 - 证据规格";
CREATE TABLE IF NOT EXISTS `pbl_blueprint_reflection_spec` (
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT ID(),
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT ,
`blueprint_id` BIGINT UNSIGNED NOT NULL COMMENT "所属蓝图",
`obj_uid` VARCHAR(32) NOT NULL COMMENT "子对象UID",
`trigger` VARCHAR(64) NOT NULL COMMENT "触发点",
`prompt_txt` TEXT NULL COMMENT "反思引导语",
`min_words` INT NOT NULL DEFAULT 0 COMMENT "最少字数",
`evidence_type` VARCHAR(64) NOT NULL COMMENT "产出证据类型",
`order_no` INT NOT NULL DEFAULT 0 COMMENT "排序",
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
PRIMARY KEY (`id`),
KEY `idx_bp_rs_bp` (`tenant_id`, `blueprint_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="蓝图子对象 - 反思规格";
CREATE TABLE IF NOT EXISTS `pbl_blueprint_version` (
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT ID(),
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT ,
`blueprint_id` BIGINT UNSIGNED NOT NULL COMMENT "蓝图ID",
`version_no` INT NOT NULL DEFAULT 0 COMMENT "版本号",
`snapshot_json` LONGTEXT NULL COMMENT "全量快照",
`change_delta_json` LONGTEXT NULL COMMENT "结构化增量",
`quality_state` VARCHAR(64) NOT NULL COMMENT "质量状态",
`generation_source` VARCHAR(64) NOT NULL COMMENT "生成来源",
`created_by` VARCHAR(32) NOT NULL COMMENT "操作人",
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
PRIMARY KEY (`id`),
UNIQUE KEY `uk_bpv` (`tenant_id`, `blueprint_id`, `version_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="蓝图版本change_delta 证明对话式改模型)";
CREATE TABLE IF NOT EXISTS `pbl_template` (
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT ID(),
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT ,
`code` VARCHAR(64) NOT NULL COMMENT "模板编码",
`title` VARCHAR(128) NOT NULL COMMENT "标题",
`subject` VARCHAR(64) NOT NULL COMMENT "学科",
`grade` VARCHAR(64) NOT NULL COMMENT "年级",
`skeleton_json` LONGTEXT NULL COMMENT "骨架",
`fallback_slot_json` LONGTEXT NULL COMMENT "兜底槽位LLM不可用时填充",
`is_builtin` TINYINT(1) NOT NULL DEFAULT 0 COMMENT "内置",
`use_count` INT NOT NULL DEFAULT 0 COMMENT "实例化次数",
`quality_state` VARCHAR(64) NOT NULL COMMENT "质量状态",
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tpl_tenant_code` (`tenant_id`, `code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="PBL 模板(含离线兜底槽位)";
-- ===== pbl_validation (M2) =====
CREATE TABLE IF NOT EXISTS `pbl_validation_rule` (
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT ID(),
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT ,
`code` VARCHAR(64) NOT NULL COMMENT "规则编码",
`dimension` VARCHAR(64) NOT NULL COMMENT "维度",
`title` VARCHAR(128) NOT NULL COMMENT "标题",
`rule_json` LONGTEXT NULL COMMENT "规则体",
`threshold_json` LONGTEXT NULL COMMENT "阈值(可配,Q-OPEN-8)",
`severity` VARCHAR(64) NOT NULL COMMENT "严重度",
`enabled` TINYINT(1) NOT NULL DEFAULT 0 COMMENT "启用",
`version_no` INT NOT NULL DEFAULT 0 COMMENT "版本",
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
PRIMARY KEY (`id`),
UNIQUE KEY `uk_vr_code` (`tenant_id`, `code`, `version_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="校验规则14 维,规则 JSON 形态 script_type=1";
CREATE TABLE IF NOT EXISTS `pbl_validation_result` (
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT ID(),
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT ,
`blueprint_id` BIGINT UNSIGNED NOT NULL COMMENT "蓝图ID",
`blueprint_version_no` INT NOT NULL DEFAULT 0 COMMENT "校验时版本",
`dimension_count` INT NOT NULL DEFAULT 0 COMMENT "维度数(=14)",
`passed_count` INT NOT NULL DEFAULT 0 COMMENT "通过数",
`failed_count` INT NOT NULL DEFAULT 0 COMMENT "失败数",
`score` DECIMAL(18,2) NOT NULL DEFAULT 0 COMMENT "得分",
`quality_state` VARCHAR(64) NOT NULL COMMENT "质量状态",
`findings_json` LONGTEXT NULL COMMENT "逐维结论",
`rule_set_version` VARCHAR(64) NOT NULL COMMENT "规则集版本",
`created_by` VARCHAR(32) NOT NULL COMMENT "触发人",
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
PRIMARY KEY (`id`),
KEY `idx_vres_bp` (`tenant_id`, `blueprint_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="校验结果14 维 + 5 级质量状态)";
-- ===== pbl_compiler (M3) =====
CREATE TABLE IF NOT EXISTS `pbl_compiler_version` (
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT ID(),
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT ,
`code` VARCHAR(64) NOT NULL COMMENT "版本编码",
`semver` VARCHAR(64) NOT NULL COMMENT "语义化版本",
`rules_hash` VARCHAR(32) NOT NULL COMMENT "规则集SHA256前缀",
`entrypoint` VARCHAR(64) NOT NULL COMMENT "入口函数",
`enabled` TINYINT(1) NOT NULL DEFAULT 0 COMMENT "启用",
`notes` TEXT NULL COMMENT "说明",
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
PRIMARY KEY (`id`),
UNIQUE KEY `uk_cv_code` (`tenant_id`, `code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="编译器版本29.6 确定性)";
CREATE TABLE IF NOT EXISTS `pbl_game_definition` (
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT ID(),
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT ,
`blueprint_id` BIGINT UNSIGNED NOT NULL COMMENT "来源蓝图",
`blueprint_version_no` INT NOT NULL DEFAULT 0 COMMENT "蓝图版本",
`compiler_version_id` BIGINT UNSIGNED NOT NULL COMMENT "编译版本",
`content_fingerprint` VARCHAR(32) NOT NULL COMMENT "SHA-256 指纹",
`definition_json` LONGTEXT NULL COMMENT "Game Definition 正文",
`world_id` BIGINT UNSIGNED NOT NULL COMMENT "落库 world",
`scene_id` BIGINT UNSIGNED NOT NULL COMMENT "落库 scene",
`entity_count` INT NOT NULL DEFAULT 0 COMMENT "实体数",
`event_count` INT NOT NULL DEFAULT 0 COMMENT "事件数",
`quality_state` VARCHAR(64) NOT NULL COMMENT "编译时质量状态",
`duration_ms` INT NOT NULL DEFAULT 0 COMMENT "耗时",
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
PRIMARY KEY (`id`),
UNIQUE KEY `uk_gd_fp` (`tenant_id`, `content_fingerprint`),
KEY `idx_gd_bp` (`tenant_id`, `blueprint_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="Game Definition第9章 schema + 指纹)";
CREATE TABLE IF NOT EXISTS `pbl_capability_registry` (
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT ID(),
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT ,
`capability_key` VARCHAR(64) NOT NULL COMMENT "能力键",
`category` VARCHAR(64) NOT NULL COMMENT "分类",
`args_schema_json` LONGTEXT NULL COMMENT "参数JSON Schema",
`permission_required` VARCHAR(64) NOT NULL COMMENT "所需权限",
`is_enabled` TINYINT(1) NOT NULL DEFAULT 0 COMMENT "启用",
`version_no` INT NOT NULL DEFAULT 0 COMMENT "版本",
`description` TEXT NULL COMMENT "说明",
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
PRIMARY KEY (`id`),
UNIQUE KEY `uk_cap_key` (`tenant_id`, `capability_key`, `version_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="Capability Registry第11章缺口补齐";
-- ===== pbl_agent_runtime (M4) =====
CREATE TABLE IF NOT EXISTS `pbl_agent_def` (
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT ID(),
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT ,
`code` VARCHAR(64) NOT NULL COMMENT "Agent编码",
`agent_type` VARCHAR(64) NOT NULL COMMENT "类型",
`name` VARCHAR(128) NOT NULL COMMENT "名称",
`model_route` VARCHAR(64) NOT NULL COMMENT "pipeline-llm 路由",
`system_prompt_txt` TEXT NULL COMMENT "提示词",
`tool_whitelist_json` LONGTEXT NULL COMMENT "工具白名单",
`permission_mode` VARCHAR(64) NOT NULL COMMENT "read/write 模式",
`is_enabled` TINYINT(1) NOT NULL DEFAULT 0 COMMENT "启用",
`version_no` INT NOT NULL DEFAULT 0 COMMENT "版本",
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
PRIMARY KEY (`id`),
UNIQUE KEY `uk_ad_code` (`tenant_id`, `code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="Agent 定义Designer/Critic";
CREATE TABLE IF NOT EXISTS `pbl_tool_registry` (
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT ID(),
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT ,
`tool_key` VARCHAR(64) NOT NULL COMMENT "工具键",
`title` VARCHAR(128) NOT NULL COMMENT "标题",
`agent_scope` VARCHAR(64) NOT NULL COMMENT "作用域",
`permission_required` VARCHAR(64) NOT NULL COMMENT "所需权限",
`enabled` TINYINT(1) NOT NULL DEFAULT 0 COMMENT "启用",
`params_schema_json` LONGTEXT NULL COMMENT "参数白名单Schema",
`disabled_reason` VARCHAR(64) NOT NULL COMMENT "禁用原因",
`version_no` INT NOT NULL DEFAULT 0 COMMENT "版本",
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tr_key` (`tenant_id`, `tool_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="工具注册表13 启用 / 9 禁用含 pbl.publish";
CREATE TABLE IF NOT EXISTS `pbl_agent_trace` (
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT ID(),
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT ,
`trace_uid` VARCHAR(32) NOT NULL COMMENT "轨迹UID",
`agent_code` VARCHAR(64) NOT NULL COMMENT "Agent",
`blueprint_id` BIGINT UNSIGNED NOT NULL COMMENT "目标蓝图",
`input_ref_json` LONGTEXT NULL COMMENT "要素1 输入引用",
`thought_txt` TEXT NULL COMMENT "要素2 思考",
`action_code` VARCHAR(64) NOT NULL COMMENT "要素3 动作",
`action_params_json` LONGTEXT NULL COMMENT "要素4 动作参数",
`observation_json` LONGTEXT NULL COMMENT "要素5 观察",
`result_state` VARCHAR(64) NOT NULL COMMENT "要素6 结果状态",
`llm_calls` INT NOT NULL DEFAULT 0 COMMENT "要素7 LLM调用数",
`duration_ms` INT NOT NULL DEFAULT 0 COMMENT "耗时",
`created_by` VARCHAR(32) NOT NULL COMMENT "触发用户",
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
PRIMARY KEY (`id`),
UNIQUE KEY `uk_at_uid` (`tenant_id`, `trace_uid`),
KEY `idx_at_tenant` (`tenant_id`, `created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="Agent 执行轨迹第28章 7 要素)";
CREATE TABLE IF NOT EXISTS `pbl_agent_tool_call` (
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT ID(),
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT ,
`trace_id` BIGINT UNSIGNED NOT NULL COMMENT "轨迹ID",
`tool_key` VARCHAR(64) NOT NULL COMMENT "工具键",
`request_json` LONGTEXT NULL COMMENT "请求",
`response_json` LONGTEXT NULL COMMENT "响应",
`ok` TINYINT(1) NOT NULL DEFAULT 0 COMMENT "成功",
`error_code` VARCHAR(64) NOT NULL COMMENT "错误码",
`adjudication_code` VARCHAR(64) NOT NULL COMMENT "裁决结论",
`duration_ms` INT NOT NULL DEFAULT 0 COMMENT "耗时",
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
PRIMARY KEY (`id`),
KEY `idx_tc_trace` (`tenant_id`, `trace_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="工具调用明细(服务端裁决留痕)";
CREATE TABLE IF NOT EXISTS `pbl_approval` (
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT ID(),
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT ,
`approval_uid` VARCHAR(32) NOT NULL COMMENT "审批UID",
`approval_type` VARCHAR(64) NOT NULL COMMENT "类型",
`target_type` VARCHAR(64) NOT NULL COMMENT "对象类型",
`target_id` BIGINT UNSIGNED NOT NULL COMMENT "对象ID",
`payload_json` LONGTEXT NULL COMMENT "变更内容",
`status` VARCHAR(64) NOT NULL COMMENT "状态",
`requested_by` VARCHAR(32) NOT NULL COMMENT "发起人",
`decided_by` VARCHAR(32) NOT NULL COMMENT "决策人",
`decided_at` DATETIME NULL NULL COMMENT "决策时间",
`comment_txt` TEXT NULL COMMENT "意见",
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
PRIMARY KEY (`id`),
UNIQUE KEY `uk_ap_uid` (`tenant_id`, `approval_uid`),
KEY `idx_ap_status` (`tenant_id`, `status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="人工审批单14.2 四类,无绕过路径)";
CREATE TABLE IF NOT EXISTS `pbl_llm_call_log` (
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT ID(),
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT ,
`call_uid` VARCHAR(32) NOT NULL COMMENT "调用UID",
`agent_code` VARCHAR(64) NOT NULL COMMENT "Agent",
`route` VARCHAR(64) NOT NULL COMMENT "路由",
`model` VARCHAR(64) NOT NULL COMMENT "实际模型",
`prompt_hash` VARCHAR(32) NOT NULL COMMENT "提示词哈希",
`completion_tokens` INT NOT NULL DEFAULT 0 COMMENT "tokens",
`latency_ms` INT NOT NULL DEFAULT 0 COMMENT "时延",
`status` VARCHAR(64) NOT NULL COMMENT "状态",
`error_code` VARCHAR(64) NOT NULL COMMENT "错误码",
`fallback_used` TINYINT(1) NOT NULL DEFAULT 0 COMMENT "走兜底",
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
PRIMARY KEY (`id`),
UNIQUE KEY `uk_lc_uid` (`tenant_id`, `call_uid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="LLM 调用日志(超时/重试/限流/兜底留痕)";
-- ===== pbl_evidence (M5) =====
CREATE TABLE IF NOT EXISTS `pbl_artifact` (
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT ID(),
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT ,
`artifact_uid` VARCHAR(32) NOT NULL COMMENT "产出UID",
`session_id` BIGINT UNSIGNED NOT NULL COMMENT "会话",
`blueprint_id` BIGINT UNSIGNED NOT NULL COMMENT "蓝图",
`team_id` BIGINT UNSIGNED NOT NULL COMMENT "团队",
`creator_id` VARCHAR(32) NOT NULL COMMENT "作者",
`title` VARCHAR(128) NOT NULL COMMENT "标题",
`artifact_type` VARCHAR(64) NOT NULL COMMENT "类型",
`content_json` LONGTEXT NULL COMMENT "内容",
`version_no` INT NOT NULL DEFAULT 0 COMMENT "版本",
`status` VARCHAR(64) NOT NULL COMMENT "状态",
`submitted_at` DATETIME NULL NULL COMMENT "提交时间",
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
PRIMARY KEY (`id`),
UNIQUE KEY `uk_art_uid` (`tenant_id`, `artifact_uid`),
KEY `idx_art_session` (`tenant_id`, `session_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="学生产出物(版本内联 version_no";
CREATE TABLE IF NOT EXISTS `pbl_evidence` (
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT ID(),
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT ,
`artifact_id` BIGINT UNSIGNED NOT NULL COMMENT "产出物",
`evidence_type` VARCHAR(64) NOT NULL COMMENT "证据类型",
`source_event_id` VARCHAR(32) NOT NULL COMMENT "来源 runtime_event",
`session_id` BIGINT UNSIGNED NOT NULL COMMENT "会话",
`learner_id` VARCHAR(32) NOT NULL COMMENT "学习者",
`blueprint_id` BIGINT UNSIGNED NOT NULL COMMENT "蓝图",
`payload_json` LONGTEXT NULL COMMENT "证据体",
`occurred_at` DATETIME NOT NULL COMMENT "发生时间",
`dedup_key` VARCHAR(32) NOT NULL COMMENT "去重键",
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
PRIMARY KEY (`id`),
UNIQUE KEY `uk_ev_dedup` (`tenant_id`, `source_event_id`, `evidence_type`),
KEY `idx_ev_learner` (`tenant_id`, `learner_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="学习证据(幂等采集,唯一索引防重)";
-- ===== pbl_assessment (M6) =====
CREATE TABLE IF NOT EXISTS `pbl_rubric` (
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT ID(),
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT ,
`code` VARCHAR(64) NOT NULL COMMENT "编码",
`title` VARCHAR(128) NOT NULL COMMENT "标题",
`blueprint_id` BIGINT UNSIGNED NOT NULL COMMENT "蓝图",
`criteria_json` LONGTEXT NULL COMMENT "准则数组(含 learning_goal_id/weight)",
`total_weight` DECIMAL(18,2) NOT NULL DEFAULT 0 COMMENT "权重合计",
`version_no` INT NOT NULL DEFAULT 0 COMMENT "版本",
`status` VARCHAR(64) NOT NULL COMMENT "状态",
`approval_id` BIGINT UNSIGNED NOT NULL COMMENT "变更审批单",
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
PRIMARY KEY (`id`),
UNIQUE KEY `uk_ru_code` (`tenant_id`, `code`, `version_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="Rubric准则内联 criteria_json权重合计=100%";
CREATE TABLE IF NOT EXISTS `pbl_assessment_record` (
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT ID(),
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT ,
`record_uid` VARCHAR(32) NOT NULL COMMENT "记录UID",
`learner_id` VARCHAR(32) NOT NULL COMMENT "学习者",
`session_id` BIGINT UNSIGNED NOT NULL COMMENT "会话",
`blueprint_id` BIGINT UNSIGNED NOT NULL COMMENT "蓝图",
`artifact_id` BIGINT UNSIGNED NOT NULL COMMENT "产出物",
`evidence_id` BIGINT UNSIGNED NOT NULL COMMENT "主证据",
`rubric_id` BIGINT UNSIGNED NOT NULL COMMENT "Rubric",
`scores_json` LONGTEXT NULL COMMENT "逐准则得分",
`total_score` DECIMAL(18,2) NOT NULL DEFAULT 0 COMMENT "加权总分",
`max_score` DECIMAL(18,2) NOT NULL DEFAULT 0 COMMENT "满分",
`band` VARCHAR(64) NOT NULL COMMENT "等级",
`alignment_json` LONGTEXT NULL COMMENT "与学习目标对齐",
`feedback_txt` TEXT NULL COMMENT "反馈",
`assessor_type` VARCHAR(64) NOT NULL COMMENT "评估者类型",
`assessor_id` VARCHAR(32) NOT NULL COMMENT "评估者",
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
PRIMARY KEY (`id`),
UNIQUE KEY `uk_ar_uid` (`tenant_id`, `record_uid`),
KEY `idx_ar_learner` (`tenant_id`, `learner_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="评估记录total_score 派生 + 对齐判定)";
-- ===== pbl_kdb_ext (M7) =====
CREATE TABLE IF NOT EXISTS `pbl_kdb_item` (
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT ID(),
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT ,
`code` VARCHAR(64) NOT NULL COMMENT "条目编码",
`item_type` VARCHAR(64) NOT NULL COMMENT "类型",
`title` VARCHAR(128) NOT NULL COMMENT "标题",
`summary_txt` TEXT NULL COMMENT "摘要",
`payload_json` LONGTEXT NULL COMMENT "内容(无向量,Q-OPEN-5)",
`source` VARCHAR(64) NOT NULL COMMENT "来源",
`is_write_locked` TINYINT(1) NOT NULL DEFAULT 0 COMMENT "写锁(恒1)",
`status` VARCHAR(64) NOT NULL COMMENT "状态",
`version_no` INT NOT NULL DEFAULT 0 COMMENT "版本",
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
PRIMARY KEY (`id`),
UNIQUE KEY `uk_kdb_code` (`tenant_id`, `code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="KDB 条目建表不写入is_write_locked 恒 1";
CREATE TABLE IF NOT EXISTS `pbl_kdb_query_log` (
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT ID(),
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT ,
`query_uid` VARCHAR(32) NOT NULL COMMENT "查询UID",
`caller_type` VARCHAR(64) NOT NULL COMMENT "调用方类型",
`caller_id` VARCHAR(32) NOT NULL COMMENT "调用方",
`query_json` LONGTEXT NULL COMMENT "查询条件",
`hit_count` INT NOT NULL DEFAULT 0 COMMENT "命中数",
`latency_ms` INT NOT NULL DEFAULT 0 COMMENT "时延",
`ok` TINYINT(1) NOT NULL DEFAULT 0 COMMENT "成功",
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
PRIMARY KEY (`id`),
UNIQUE KEY `uk_kql_uid` (`tenant_id`, `query_uid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="KDB 检索日志(只读侧留痕)";
-- ===== pbl_domain_ext (M8) =====
CREATE TABLE IF NOT EXISTS `pbl_tenant` (
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT ID(),
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT ,
`tenant_uid` VARCHAR(32) NOT NULL COMMENT "租户UID",
`name` VARCHAR(128) NOT NULL COMMENT "名称",
`kind` VARCHAR(64) NOT NULL COMMENT "类型",
`plan` VARCHAR(64) NOT NULL COMMENT "套餐",
`seat_limit` INT NOT NULL DEFAULT 0 COMMENT "席位",
`status` VARCHAR(64) NOT NULL COMMENT "状态",
`config_json` LONGTEXT NULL COMMENT "配置",
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
PRIMARY KEY (`id`),
UNIQUE KEY `uk_ten_uid` (`tenant_id`, `tenant_uid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="租户多租户边界第25章不改基表";
CREATE TABLE IF NOT EXISTS `pbl_class` (
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT ID(),
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT ,
`class_uid` VARCHAR(32) NOT NULL COMMENT "班级UID",
`name` VARCHAR(128) NOT NULL COMMENT "名称",
`grade` VARCHAR(64) NOT NULL COMMENT "年级",
`teacher_id` VARCHAR(32) NOT NULL COMMENT "教师",
`student_count` INT NOT NULL DEFAULT 0 COMMENT "学生数",
`roster_json` LONGTEXT NULL COMMENT "花名册",
`status` VARCHAR(64) NOT NULL COMMENT "状态",
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
PRIMARY KEY (`id`),
UNIQUE KEY `uk_cls_uid` (`tenant_id`, `class_uid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="班级(花名册内联 roster_json";
CREATE TABLE IF NOT EXISTS `pbl_team` (
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT ID(),
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT ,
`team_uid` VARCHAR(32) NOT NULL COMMENT "团队UID",
`class_id` BIGINT UNSIGNED NOT NULL COMMENT "班级",
`name` VARCHAR(128) NOT NULL COMMENT "名称",
`mission_id` BIGINT UNSIGNED NOT NULL COMMENT "Mission",
`members_json` LONGTEXT NULL COMMENT "成员",
`capacity` INT NOT NULL DEFAULT 0 COMMENT "容量",
`status` VARCHAR(64) NOT NULL COMMENT "状态",
`session_id` BIGINT UNSIGNED NOT NULL COMMENT "绑定会话",
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
PRIMARY KEY (`id`),
UNIQUE KEY `uk_team_uid` (`tenant_id`, `team_uid`),
KEY `idx_team_class` (`tenant_id`, `class_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="团队(成员内联 members_json不改基表";
-- ===== pbl_scense_ext (M9) =====
CREATE TABLE IF NOT EXISTS `pbl_session_member` (
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT ID(),
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT ,
`session_id` BIGINT UNSIGNED NOT NULL COMMENT "会话",
`user_id` VARCHAR(32) NOT NULL COMMENT "用户",
`role_code` VARCHAR(64) NOT NULL COMMENT "角色",
`team_id` BIGINT UNSIGNED NOT NULL COMMENT "团队",
`joined_at` DATETIME NULL NULL COMMENT "加入时间",
`last_seen_at` DATETIME NULL NULL COMMENT "心跳",
`state_version` BIGINT NOT NULL DEFAULT 0 COMMENT "已见状态版本",
`status` VARCHAR(64) NOT NULL COMMENT "状态",
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
PRIMARY KEY (`id`),
UNIQUE KEY `uk_sm` (`tenant_id`, `session_id`, `user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="会话成员(复用 world_session不污染基表";
CREATE TABLE IF NOT EXISTS `pbl_playtest_feedback` (
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT ID(),
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT ,
`session_id` BIGINT UNSIGNED NOT NULL COMMENT "会话",
`user_id` VARCHAR(32) NOT NULL COMMENT "用户",
`blueprint_id` BIGINT UNSIGNED NOT NULL COMMENT "蓝图",
`feedback_type` VARCHAR(64) NOT NULL COMMENT "类型",
`rating` INT NOT NULL DEFAULT 0 COMMENT "评分",
`content_txt` TEXT NULL COMMENT "内容",
`context_json` LONGTEXT NULL COMMENT "上下文",
`handled` TINYINT(1) NOT NULL DEFAULT 0 COMMENT "已处理",
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
PRIMARY KEY (`id`),
KEY `idx_pf_session` (`tenant_id`, `session_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="Playtest 反馈/反思reflection 走证据链外的轻量表)";
CREATE TABLE IF NOT EXISTS `pbl_ui_page_registry` (
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT ID(),
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT ,
`page_key` VARCHAR(64) NOT NULL COMMENT "页面键",
`title` VARCHAR(128) NOT NULL COMMENT "标题",
`route` VARCHAR(64) NOT NULL COMMENT "路由",
`widget_type` VARCHAR(64) NOT NULL COMMENT "渲染组件",
`requires_role` VARCHAR(64) NOT NULL COMMENT "所需角色",
`budget_ms` INT NOT NULL DEFAULT 0 COMMENT "渲染预算",
`enabled` TINYINT(1) NOT NULL DEFAULT 0 COMMENT "启用",
`meta_json` LONGTEXT NULL COMMENT "元数据",
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
PRIMARY KEY (`id`),
UNIQUE KEY `uk_upr_key` (`tenant_id`, `page_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="前端覆盖层页面注册12 页面 + 时延预算)";
-- ===== pbl_runtime_ext (M11) =====
CREATE TABLE IF NOT EXISTS `pbl_runtime_event` (
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT ID(),
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT ,
`event_uid` VARCHAR(32) NOT NULL COMMENT "事件UID",
`session_id` BIGINT UNSIGNED NOT NULL COMMENT "会话",
`world_id` BIGINT UNSIGNED NOT NULL COMMENT "世界",
`actor_id` VARCHAR(32) NOT NULL COMMENT "触发者",
`event_type` VARCHAR(64) NOT NULL COMMENT "事件类型",
`payload_json` LONGTEXT NULL COMMENT "事件体",
`causation_id` VARCHAR(32) NOT NULL COMMENT "因果链",
`state_version` BIGINT NOT NULL DEFAULT 0 COMMENT "状态版本",
`seq` BIGINT NOT NULL DEFAULT 0 COMMENT "会话内单调序号",
`occurred_at` DATETIME NOT NULL COMMENT "发生时间",
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
PRIMARY KEY (`id`),
UNIQUE KEY `uk_re_uid` (`tenant_id`, `event_uid`, `occurred_at`),
KEY `idx_re_session_seq` (`tenant_id`, `session_id`, `seq`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="运行时事件append-only按月 RANGE 分区)",
KEY `idx_re_part` (`occurred_at`)
PARTITION BY RANGE COLUMNS(`occurred_at`) (
PARTITION p2026 VALUES LESS THAN ('2026-01-01'),
PARTITION p2027 VALUES LESS THAN ('2027-01-01'),
PARTITION p2028 VALUES LESS THAN ('2028-01-01'),
PARTITION p2029 VALUES LESS THAN ('2029-01-01'),
PARTITION p2030 VALUES LESS THAN ('2030-01-01'),
PARTITION p2031 VALUES LESS THAN ('2031-01-01')
PARTITION pmax VALUES LESS THAN (MAXVALUE)
);
CREATE TABLE IF NOT EXISTS `pbl_entity_state` (
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT ID(),
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT ,
`session_id` BIGINT UNSIGNED NOT NULL COMMENT "会话",
`entity_id` VARCHAR(32) NOT NULL COMMENT "实体标识",
`state_json` LONGTEXT NULL COMMENT "状态",
`state_version` BIGINT NOT NULL DEFAULT 0 COMMENT "单调版本",
`checksum` VARCHAR(32) NOT NULL COMMENT "状态摘要",
`updated_by` VARCHAR(32) NOT NULL COMMENT "写入者",
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
PRIMARY KEY (`id`),
UNIQUE KEY `uk_es` (`tenant_id`, `session_id`, `entity_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="实体状态服务端权威state_version 客户端禁写)";
CREATE TABLE IF NOT EXISTS `pbl_world_state_snapshot` (
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT ID(),
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT ,
`session_id` BIGINT UNSIGNED NOT NULL COMMENT "会话",
`snapshot_uid` VARCHAR(32) NOT NULL COMMENT "快照UID",
`state_fingerprint` VARCHAR(32) NOT NULL COMMENT "状态指纹",
`entities_json` LONGTEXT NULL COMMENT "实体集",
`events_through_seq` BIGINT NOT NULL DEFAULT 0 COMMENT "截至序号",
`reason` VARCHAR(64) NOT NULL COMMENT "触发原因",
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
PRIMARY KEY (`id`),
UNIQUE KEY `uk_wss_uid` (`tenant_id`, `snapshot_uid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="世界状态快照(掉线补齐/离线兜底)";
-- ===== 枚举 6+2 组由 pbl_appcodes.load_pbl_appcodes() 幂等注入 appcodes本文件不含 INSERT =====

733
scripts/gen_artifacts.py Normal file
View File

@ -0,0 +1,733 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
pbls 交付物机械生成器单一事实源 DDL / models / CRUD json / dspy / 模块包骨架 / RBAC 路径
为什么存在QC 退回意见 #8/#9/#11 要求「表定义四段式 models/*.json」「CRUD json/*.json」
RBAC 路径注册可机械核对手写 36 × 3 份产物必然漂移故用本脚本从一份 spec 生成
保证 DDL models/CRUD/RBAC 完全一致可重复执行幂等
方言来源projects/pbls/env/test.json -> db.engine = mariadb TYPE_MAP BIGSERIAL/SERIAL/nextval
"""
import json
import os
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.abspath(os.path.join(HERE, '..', '..', '..')) # 机构工作空间根
APP = os.path.join(ROOT, 'apps', 'pbls')
MODS = os.path.join(ROOT, 'modules')
# ── 抽象类型 → MariaDB 10.x 物理类型映射唯一映射表QC #5/#8 核对用)────────────
TYPE_MAP = {
'pk': 'BIGINT UNSIGNED NOT NULL AUTO_INCREMENT',
'tenant': 'VARCHAR(32) NOT NULL',
'sid': 'VARCHAR(32) NOT NULL',
'code': 'VARCHAR(64) NOT NULL',
'name': 'VARCHAR(128) NOT NULL',
'txt': 'TEXT',
'js': 'LONGTEXT',
'int': 'INT NOT NULL DEFAULT 0',
'bigint': 'BIGINT NOT NULL DEFAULT 0',
'dec': 'DECIMAL(18,2) NOT NULL DEFAULT 0',
'bool': 'TINYINT(1) NOT NULL DEFAULT 0',
'ts': 'DATETIME NULL',
'tsnn': 'DATETIME NOT NULL',
'ref': 'BIGINT UNSIGNED NOT NULL',
'sref': 'VARCHAR(32) NOT NULL',
}
ABSTRACT = {
'pk': {'type': 'int', 'length': 20, 'unsigned': True, 'auto_increment': True},
'tenant': {'type': 'str', 'length': 32},
'sid': {'type': 'str', 'length': 32},
'code': {'type': 'str', 'length': 64},
'name': {'type': 'str', 'length': 128},
'txt': {'type': 'text'},
'js': {'type': 'json'},
'int': {'type': 'int'},
'bigint': {'type': 'bigint'},
'dec': {'type': 'double', 'precision': 18, 'scale': 2},
'bool': {'type': 'bool'},
'ts': {'type': 'datetime'},
'tsnn': {'type': 'datetime'},
'ref': {'type': 'bigint'},
'sref': {'type': 'str', 'length': 32},
}
NULLABLE = {'txt', 'js', 'ts'}
# ── 模块清单:编号 ↔ 目录 ↔ 表 ↔ 契约QC #13 对账表的事实源)───────────────────
# tables=[] 且 contracts 不含 CRUD 的模块为「零表模块」pbl_common / pbl_appcodes
MODULES = {
'pbl_common': dict(no='M12', tables=[], contracts=[
'pbl_common_ping', 'pbl_common_audit']),
'pbl_appcodes': dict(no='M10', tables=[], contracts=[
'pbl_appcodes_inject', 'pbl_appcodes_list']),
'pbl_blueprint': dict(no='M1', tables=[
'pbl_blueprint', 'pbl_blueprint_learning_goal', 'pbl_blueprint_role',
'pbl_blueprint_mission', 'pbl_blueprint_task', 'pbl_blueprint_artifact_spec',
'pbl_blueprint_evidence_spec', 'pbl_blueprint_reflection_spec',
'pbl_blueprint_version', 'pbl_template'],
contracts=[
'pbl_blueprint_create', 'pbl_blueprint_read', 'pbl_blueprint_update',
'pbl_blueprint_delete', 'pbl_blueprint_list', 'pbl_blueprint_tree',
'pbl_blueprint_fork', 'pbl_blueprint_subobject_save', 'pbl_blueprint_subobject_list',
'pbl_blueprint_version_create', 'pbl_blueprint_version_diff',
'pbl_template_list', 'pbl_template_instantiate']),
'pbl_validation': dict(no='M2', tables=['pbl_validation_rule', 'pbl_validation_result'],
contracts=['pbl_validation_run', 'pbl_validation_get', 'pbl_validation_list',
'pbl_validation_rule_list', 'pbl_validation_rule_save']),
'pbl_compiler': dict(no='M3', tables=['pbl_compiler_version', 'pbl_game_definition',
'pbl_capability_registry'],
contracts=['pbl_compiler_compile', 'pbl_compiler_preview', 'pbl_compiler_compare',
'pbl_compiler_version_list', 'pbl_compiler_version_save',
'pbl_capability_list', 'pbl_capability_register']),
'pbl_agent_runtime': dict(no='M4', tables=['pbl_agent_def', 'pbl_tool_registry',
'pbl_agent_trace', 'pbl_agent_tool_call', 'pbl_approval', 'pbl_llm_call_log'],
contracts=['pbl_agent_designer_run', 'pbl_agent_critic_run', 'pbl_agent_trace_write',
'pbl_agent_trace_list', 'pbl_tool_registry_list', 'pbl_tool_registry_save',
'pbl_tool_adjudicate', 'pbl_approval_create', 'pbl_approval_decide',
'pbl_approval_list']),
'pbl_evidence': dict(no='M5', tables=['pbl_artifact', 'pbl_evidence'],
contracts=['pbl_artifact_create', 'pbl_artifact_read', 'pbl_artifact_update',
'pbl_artifact_delete', 'pbl_artifact_list', 'pbl_evidence_collect',
'pbl_evidence_list']),
'pbl_assessment': dict(no='M6', tables=['pbl_rubric', 'pbl_assessment_record'],
contracts=['pbl_rubric_save', 'pbl_rubric_get', 'pbl_rubric_list',
'pbl_assessment_score', 'pbl_assessment_record_get', 'pbl_assessment_report']),
'pbl_kdb_ext': dict(no='M7', tables=['pbl_kdb_item', 'pbl_kdb_query_log'],
contracts=['pbl_kdb_search', 'pbl_kdb_get', 'pbl_research_aggregate']),
'pbl_domain_ext': dict(no='M8', tables=['pbl_tenant', 'pbl_class', 'pbl_team'],
contracts=['pbl_tenant_upsert', 'pbl_class_save', 'pbl_class_list',
'pbl_team_save', 'pbl_team_list',
'pbl_domain_materialize_game_definition']),
'pbl_scense_ext': dict(no='M9', tables=['pbl_session_member', 'pbl_playtest_feedback',
'pbl_ui_page_registry'],
contracts=['pbl_session_context_get', 'pbl_page_registry_list',
'pbl_playtest_feedback_save', 'pbl_overlay_bootstrap']),
'pbl_runtime_ext': dict(no='M11', tables=['pbl_runtime_event', 'pbl_entity_state',
'pbl_world_state_snapshot'],
contracts=['pbl_runtime_event_append', 'pbl_runtime_event_poll',
'pbl_entity_state_get', 'pbl_entity_state_apply',
'pbl_world_broadcast', 'pbl_session_member_add']),
}
# ── 36 表定义(字段: (name, 抽象类型, 注释[, null]))────────────────────────────
T = {}
def tbl(mod, name, comment, fields, indexes, codes=None, partition_month=None):
T[name] = dict(module=mod, comment=comment, fields=fields, indexes=indexes,
codes=codes or [], partition_month=partition_month)
tbl('pbl_blueprint', 'pbl_blueprint', 'PBL 蓝图聚合根第8章 schema 落库)', [
('code', 'code', '蓝图编码'), ('title', 'name', '标题'), ('subject', 'code', '学科'),
('grade', 'code', '年级'), ('description', 'txt', '说明'),
('quality_state', 'code', '质量状态 pbl_quality_state'),
('generation_source', 'code', '生成来源 llm/template_fallback/manual'),
('approval_status', 'code', '审批状态'), ('visibility', 'code', '可见范围'),
('content_json', 'js', '蓝图正文(结构化模型,非散文)'),
('version_no', 'int', '当前版本'), ('compiler_version_id', 'ref', '编译版本', True),
('world_id', 'ref', '编译产物落 world 的溯源ID', True),
('created_by', 'sref', '创建人')],
[('uk_bp_tenant_code', ['tenant_id', 'code'], True),
('idx_bp_tenant_state', ['tenant_id', 'quality_state'], False),
('idx_bp_tenant_creator', ['tenant_id', 'created_by'], False)],
[('quality_state', 'pbl_quality_state'), ('approval_status', 'pbl_approval_status'),
('visibility', 'pbl_visibility')])
SUB = [('blueprint_id', 'ref', '所属蓝图'), ('obj_uid', 'sid', '子对象UID')]
for mod_tbl, extra, cmt in [
('pbl_blueprint_learning_goal', [('code', 'code', '目标编码'), ('title', 'name', '标题'),
('description', 'txt', '描述'), ('bloom_level', 'code', '布鲁姆层级'),
('measurable', 'bool', '是否可测'), ('order_no', 'int', '排序')], '学习目标'),
('pbl_blueprint_role', [('code', 'code', '角色编码'), ('title', 'name', '角色名'),
('persona_txt', 'txt', '人设'), ('capabilities_json', 'js', '能力引用'),
('headcount', 'int', '名额'), ('order_no', 'int', '排序')], 'PBL 角色'),
('pbl_blueprint_mission', [('code', 'code', '任务域编码'), ('title', 'name', '驱动问题域'),
('scenario_txt', 'txt', '情境'), ('unlock_condition_json', 'js', '解锁条件'),
('order_no', 'int', '排序')], '驱动问题/Mission'),
('pbl_blueprint_task', [('mission_id', 'ref', '所属Mission'), ('code', 'code', '编码'),
('title', 'name', '任务名'), ('steps_json', 'js', '步骤'),
('difficulty', 'code', '难度'), ('order_no', 'int', '排序')], '任务'),
('pbl_blueprint_artifact_spec', [('code', 'code', '编码'), ('title', 'name', '产出物名'),
('artifact_type', 'code', '产出类型'), ('required_fields_json', 'js', '必填字段'),
('order_no', 'int', '排序')], '产出物规格'),
('pbl_blueprint_evidence_spec', [('evidence_type', 'code', '证据类型'),
('source', 'code', '来源'), ('min_count', 'int', '最少条数'),
('rubric_criterion_uid', 'sid', '对齐的评分准则UID', True),
('order_no', 'int', '排序')], '证据规格')]:
tbl('pbl_blueprint', mod_tbl, '蓝图子对象 - ' + cmt, SUB + extra,
[('idx_%s_bp' % mod_tbl[4:], ['tenant_id', 'blueprint_id'], False)])
tbl('pbl_blueprint', 'pbl_blueprint_reflection_spec', '蓝图子对象 - 反思规格',
SUB + [('trigger', 'code', '触发点'), ('prompt_txt', 'txt', '反思引导语'),
('min_words', 'int', '最少字数'), ('evidence_type', 'code', '产出证据类型'),
('order_no', 'int', '排序')],
[('idx_bp_rs_bp', ['tenant_id', 'blueprint_id'], False)])
tbl('pbl_blueprint', 'pbl_blueprint_version', '蓝图版本change_delta 证明对话式改模型)', [
('blueprint_id', 'ref', '蓝图ID'), ('version_no', 'int', '版本号'),
('snapshot_json', 'js', '全量快照'), ('change_delta_json', 'js', '结构化增量'),
('quality_state', 'code', '质量状态'), ('generation_source', 'code', '生成来源'),
('created_by', 'sref', '操作人')],
[('uk_bpv', ['tenant_id', 'blueprint_id', 'version_no'], True)])
tbl('pbl_blueprint', 'pbl_template', 'PBL 模板(含离线兜底槽位)', [
('code', 'code', '模板编码'), ('title', 'name', '标题'), ('subject', 'code', '学科'),
('grade', 'code', '年级'), ('skeleton_json', 'js', '骨架'),
('fallback_slot_json', 'js', '兜底槽位LLM不可用时填充'),
('is_builtin', 'bool', '内置'), ('use_count', 'int', '实例化次数'),
('quality_state', 'code', '质量状态')],
[('uk_tpl_tenant_code', ['tenant_id', 'code'], True)])
tbl('pbl_validation', 'pbl_validation_rule', '校验规则14 维,规则 JSON 形态 script_type=1', [
('code', 'code', '规则编码'), ('dimension', 'code', '维度'), ('title', 'name', '标题'),
('rule_json', 'js', '规则体'), ('threshold_json', 'js', '阈值(可配,Q-OPEN-8)'),
('severity', 'code', '严重度'), ('enabled', 'bool', '启用'), ('version_no', 'int', '版本')],
[('uk_vr_code', ['tenant_id', 'code', 'version_no'], True)])
tbl('pbl_validation', 'pbl_validation_result', '校验结果14 维 + 5 级质量状态)', [
('blueprint_id', 'ref', '蓝图ID'), ('blueprint_version_no', 'int', '校验时版本'),
('dimension_count', 'int', '维度数(=14)'), ('passed_count', 'int', '通过数'),
('failed_count', 'int', '失败数'), ('score', 'dec', '得分'),
('quality_state', 'code', '质量状态'), ('findings_json', 'js', '逐维结论'),
('rule_set_version', 'code', '规则集版本'), ('created_by', 'sref', '触发人')],
[('idx_vres_bp', ['tenant_id', 'blueprint_id'], False)])
tbl('pbl_compiler', 'pbl_compiler_version', '编译器版本29.6 确定性)', [
('code', 'code', '版本编码'), ('semver', 'code', '语义化版本'),
('rules_hash', 'sid', '规则集SHA256前缀'), ('entrypoint', 'code', '入口函数'),
('enabled', 'bool', '启用'), ('notes', 'txt', '说明')],
[('uk_cv_code', ['tenant_id', 'code'], True)])
tbl('pbl_compiler', 'pbl_game_definition', 'Game Definition第9章 schema + 指纹)', [
('blueprint_id', 'ref', '来源蓝图'), ('blueprint_version_no', 'int', '蓝图版本'),
('compiler_version_id', 'ref', '编译版本'), ('content_fingerprint', 'sid', 'SHA-256 指纹'),
('definition_json', 'js', 'Game Definition 正文'),
('world_id', 'ref', '落库 world', True), ('scene_id', 'ref', '落库 scene', True),
('entity_count', 'int', '实体数'), ('event_count', 'int', '事件数'),
('quality_state', 'code', '编译时质量状态'), ('duration_ms', 'int', '耗时')],
[('uk_gd_fp', ['tenant_id', 'content_fingerprint'], True),
('idx_gd_bp', ['tenant_id', 'blueprint_id'], False)])
tbl('pbl_compiler', 'pbl_capability_registry', 'Capability Registry第11章缺口补齐', [
('capability_key', 'code', '能力键'), ('category', 'code', '分类'),
('args_schema_json', 'js', '参数JSON Schema'), ('permission_required', 'code', '所需权限'),
('is_enabled', 'bool', '启用'), ('version_no', 'int', '版本'), ('description', 'txt', '说明')],
[('uk_cap_key', ['tenant_id', 'capability_key', 'version_no'], True)])
tbl('pbl_agent_runtime', 'pbl_agent_def', 'Agent 定义Designer/Critic', [
('code', 'code', 'Agent编码'), ('agent_type', 'code', '类型'), ('name', 'name', '名称'),
('model_route', 'code', 'pipeline-llm 路由'), ('system_prompt_txt', 'txt', '提示词'),
('tool_whitelist_json', 'js', '工具白名单'), ('permission_mode', 'code', 'read/write 模式'),
('is_enabled', 'bool', '启用'), ('version_no', 'int', '版本')],
[('uk_ad_code', ['tenant_id', 'code'], True)])
tbl('pbl_agent_runtime', 'pbl_tool_registry', '工具注册表13 启用 / 9 禁用含 pbl.publish', [
('tool_key', 'code', '工具键'), ('title', 'name', '标题'), ('agent_scope', 'code', '作用域'),
('permission_required', 'code', '所需权限'), ('enabled', 'bool', '启用'),
('params_schema_json', 'js', '参数白名单Schema'), ('disabled_reason', 'code', '禁用原因', True),
('version_no', 'int', '版本')],
[('uk_tr_key', ['tenant_id', 'tool_key'], True)])
tbl('pbl_agent_runtime', 'pbl_agent_trace', 'Agent 执行轨迹第28章 7 要素)', [
('trace_uid', 'sid', '轨迹UID'), ('agent_code', 'code', 'Agent'),
('blueprint_id', 'ref', '目标蓝图', True), ('input_ref_json', 'js', '要素1 输入引用'),
('thought_txt', 'txt', '要素2 思考'), ('action_code', 'code', '要素3 动作'),
('action_params_json', 'js', '要素4 动作参数'), ('observation_json', 'js', '要素5 观察'),
('result_state', 'code', '要素6 结果状态'), ('llm_calls', 'int', '要素7 LLM调用数'),
('duration_ms', 'int', '耗时'), ('created_by', 'sref', '触发用户')],
[('uk_at_uid', ['tenant_id', 'trace_uid'], True),
('idx_at_tenant', ['tenant_id', 'created_at'], False)])
tbl('pbl_agent_runtime', 'pbl_agent_tool_call', '工具调用明细(服务端裁决留痕)', [
('trace_id', 'ref', '轨迹ID'), ('tool_key', 'code', '工具键'),
('request_json', 'js', '请求'), ('response_json', 'js', '响应'),
('ok', 'bool', '成功'), ('error_code', 'code', '错误码', True),
('adjudication_code', 'code', '裁决结论'), ('duration_ms', 'int', '耗时')],
[('idx_tc_trace', ['tenant_id', 'trace_id'], False)])
tbl('pbl_agent_runtime', 'pbl_approval', '人工审批单14.2 四类,无绕过路径)', [
('approval_uid', 'sid', '审批UID'), ('approval_type', 'code', '类型'),
('target_type', 'code', '对象类型'), ('target_id', 'ref', '对象ID'),
('payload_json', 'js', '变更内容'), ('status', 'code', '状态'),
('requested_by', 'sref', '发起人'), ('decided_by', 'sref', '决策人', True),
('decided_at', 'ts', '决策时间'), ('comment_txt', 'txt', '意见')],
[('uk_ap_uid', ['tenant_id', 'approval_uid'], True),
('idx_ap_status', ['tenant_id', 'status'], False)],
[('status', 'pbl_approval_status')])
tbl('pbl_agent_runtime', 'pbl_llm_call_log', 'LLM 调用日志(超时/重试/限流/兜底留痕)', [
('call_uid', 'sid', '调用UID'), ('agent_code', 'code', 'Agent'), ('route', 'code', '路由'),
('model', 'code', '实际模型', True), ('prompt_hash', 'sid', '提示词哈希'),
('completion_tokens', 'int', 'tokens'), ('latency_ms', 'int', '时延'),
('status', 'code', '状态'), ('error_code', 'code', '错误码', True),
('fallback_used', 'bool', '走兜底')],
[('uk_lc_uid', ['tenant_id', 'call_uid'], True)])
tbl('pbl_evidence', 'pbl_artifact', '学生产出物(版本内联 version_no', [
('artifact_uid', 'sid', '产出UID'), ('session_id', 'ref', '会话'),
('blueprint_id', 'ref', '蓝图'), ('team_id', 'ref', '团队', True),
('creator_id', 'sref', '作者'), ('title', 'name', '标题'),
('artifact_type', 'code', '类型'), ('content_json', 'js', '内容'),
('version_no', 'int', '版本'), ('status', 'code', '状态'), ('submitted_at', 'ts', '提交时间')],
[('uk_art_uid', ['tenant_id', 'artifact_uid'], True),
('idx_art_session', ['tenant_id', 'session_id'], False)],
[('artifact_type', 'pbl_artifact_type')])
tbl('pbl_evidence', 'pbl_evidence', '学习证据(幂等采集,唯一索引防重)', [
('artifact_id', 'ref', '产出物', True), ('evidence_type', 'code', '证据类型'),
('source_event_id', 'sid', '来源 runtime_event'), ('session_id', 'ref', '会话'),
('learner_id', 'sref', '学习者'), ('blueprint_id', 'ref', '蓝图', True),
('payload_json', 'js', '证据体'), ('occurred_at', 'tsnn', '发生时间'),
('dedup_key', 'sid', '去重键')],
[('uk_ev_dedup', ['tenant_id', 'source_event_id', 'evidence_type'], True),
('idx_ev_learner', ['tenant_id', 'learner_id'], False)],
[('evidence_type', 'pbl_evidence_type')])
tbl('pbl_assessment', 'pbl_rubric', 'Rubric准则内联 criteria_json权重合计=100%', [
('code', 'code', '编码'), ('title', 'name', '标题'), ('blueprint_id', 'ref', '蓝图', True),
('criteria_json', 'js', '准则数组(含 learning_goal_id/weight)'),
('total_weight', 'dec', '权重合计'), ('version_no', 'int', '版本'),
('status', 'code', '状态'), ('approval_id', 'ref', '变更审批单', True)],
[('uk_ru_code', ['tenant_id', 'code', 'version_no'], True)])
tbl('pbl_assessment', 'pbl_assessment_record', '评估记录total_score 派生 + 对齐判定)', [
('record_uid', 'sid', '记录UID'), ('learner_id', 'sref', '学习者'),
('session_id', 'ref', '会话', True), ('blueprint_id', 'ref', '蓝图', True),
('artifact_id', 'ref', '产出物', True), ('evidence_id', 'ref', '主证据', True),
('rubric_id', 'ref', 'Rubric'), ('scores_json', 'js', '逐准则得分'),
('total_score', 'dec', '加权总分'), ('max_score', 'dec', '满分'),
('band', 'code', '等级'), ('alignment_json', 'js', '与学习目标对齐'),
('feedback_txt', 'txt', '反馈'), ('assessor_type', 'code', '评估者类型'),
('assessor_id', 'sref', '评估者')],
[('uk_ar_uid', ['tenant_id', 'record_uid'], True),
('idx_ar_learner', ['tenant_id', 'learner_id'], False)])
tbl('pbl_kdb_ext', 'pbl_kdb_item', 'KDB 条目建表不写入is_write_locked 恒 1', [
('code', 'code', '条目编码'), ('item_type', 'code', '类型'), ('title', 'name', '标题'),
('summary_txt', 'txt', '摘要'), ('payload_json', 'js', '内容(无向量,Q-OPEN-5)'),
('source', 'code', '来源'), ('is_write_locked', 'bool', '写锁(恒1)'),
('status', 'code', '状态'), ('version_no', 'int', '版本')],
[('uk_kdb_code', ['tenant_id', 'code'], True)])
tbl('pbl_kdb_ext', 'pbl_kdb_query_log', 'KDB 检索日志(只读侧留痕)', [
('query_uid', 'sid', '查询UID'), ('caller_type', 'code', '调用方类型'),
('caller_id', 'sref', '调用方'), ('query_json', 'js', '查询条件'),
('hit_count', 'int', '命中数'), ('latency_ms', 'int', '时延'), ('ok', 'bool', '成功')],
[('uk_kql_uid', ['tenant_id', 'query_uid'], True)])
tbl('pbl_domain_ext', 'pbl_tenant', '租户多租户边界第25章不改基表', [
('tenant_uid', 'sid', '租户UID'), ('name', 'name', '名称'), ('kind', 'code', '类型'),
('plan', 'code', '套餐'), ('seat_limit', 'int', '席位'), ('status', 'code', '状态'),
('config_json', 'js', '配置')],
[('uk_ten_uid', ['tenant_id', 'tenant_uid'], True)])
tbl('pbl_domain_ext', 'pbl_class', '班级(花名册内联 roster_json', [
('class_uid', 'sid', '班级UID'), ('name', 'name', '名称'), ('grade', 'code', '年级'),
('teacher_id', 'sref', '教师'), ('student_count', 'int', '学生数'),
('roster_json', 'js', '花名册'), ('status', 'code', '状态')],
[('uk_cls_uid', ['tenant_id', 'class_uid'], True)])
tbl('pbl_domain_ext', 'pbl_team', '团队(成员内联 members_json不改基表', [
('team_uid', 'sid', '团队UID'), ('class_id', 'ref', '班级'), ('name', 'name', '名称'),
('mission_id', 'ref', 'Mission', True), ('members_json', 'js', '成员'),
('capacity', 'int', '容量'), ('status', 'code', '状态'),
('session_id', 'ref', '绑定会话', True)],
[('uk_team_uid', ['tenant_id', 'team_uid'], True),
('idx_team_class', ['tenant_id', 'class_id'], False)])
tbl('pbl_scense_ext', 'pbl_session_member', '会话成员(复用 world_session不污染基表', [
('session_id', 'ref', '会话'), ('user_id', 'sref', '用户'), ('role_code', 'code', '角色'),
('team_id', 'ref', '团队', True), ('joined_at', 'ts', '加入时间'),
('last_seen_at', 'ts', '心跳'), ('state_version', 'bigint', '已见状态版本'),
('status', 'code', '状态')],
[('uk_sm', ['tenant_id', 'session_id', 'user_id'], True)])
tbl('pbl_scense_ext', 'pbl_playtest_feedback', 'Playtest 反馈/反思reflection 走证据链外的轻量表)', [
('session_id', 'ref', '会话', True), ('user_id', 'sref', '用户'),
('blueprint_id', 'ref', '蓝图', True), ('feedback_type', 'code', '类型'),
('rating', 'int', '评分'), ('content_txt', 'txt', '内容'),
('context_json', 'js', '上下文'), ('handled', 'bool', '已处理')],
[('idx_pf_session', ['tenant_id', 'session_id'], False)])
tbl('pbl_scense_ext', 'pbl_ui_page_registry', '前端覆盖层页面注册12 页面 + 时延预算)', [
('page_key', 'code', '页面键'), ('title', 'name', '标题'), ('route', 'code', '路由'),
('widget_type', 'code', '渲染组件'), ('requires_role', 'code', '所需角色'),
('budget_ms', 'int', '渲染预算'), ('enabled', 'bool', '启用'), ('meta_json', 'js', '元数据')],
[('uk_upr_key', ['tenant_id', 'page_key'], True)])
tbl('pbl_runtime_ext', 'pbl_runtime_event', '运行时事件append-only按月 RANGE 分区)', [
('event_uid', 'sid', '事件UID'), ('session_id', 'ref', '会话'),
('world_id', 'ref', '世界'), ('actor_id', 'sref', '触发者'),
('event_type', 'code', '事件类型'), ('payload_json', 'js', '事件体'),
('causation_id', 'sid', '因果链', True), ('state_version', 'bigint', '状态版本'),
('seq', 'bigint', '会话内单调序号'), ('occurred_at', 'tsnn', '发生时间')],
[('uk_re_uid', ['tenant_id', 'event_uid', 'occurred_at'], True),
('idx_re_session_seq', ['tenant_id', 'session_id', 'seq'], False)],
[('event_type', 'pbl_event_type')], partition_month=True)
tbl('pbl_runtime_ext', 'pbl_entity_state', '实体状态服务端权威state_version 客户端禁写)', [
('session_id', 'ref', '会话'), ('entity_id', 'sid', '实体标识'),
('state_json', 'js', '状态'), ('state_version', 'bigint', '单调版本'),
('checksum', 'sid', '状态摘要'), ('updated_by', 'sref', '写入者')],
[('uk_es', ['tenant_id', 'session_id', 'entity_id'], True)])
tbl('pbl_runtime_ext', 'pbl_world_state_snapshot', '世界状态快照(掉线补齐/离线兜底)', [
('session_id', 'ref', '会话'), ('snapshot_uid', 'sid', '快照UID'),
('state_fingerprint', 'sid', '状态指纹'), ('entities_json', 'js', '实体集'),
('events_through_seq', 'bigint', '截至序号'), ('reason', 'code', '触发原因')],
[('uk_wss_uid', ['tenant_id', 'snapshot_uid'], True)])
assert len(T) == 36, '表数应为 36实际 %d' % len(T)
# ── 枚举 6+2 组appcodes.md 幂等注入,零表模块 pbl_appcodes 的事实源)────────────
APPCODES = {
'pbl_quality_state': ['draft', 'needs_work', 'pbl_ready', 'playtest_ready', 'publish_ready'],
'pbl_approval_status': ['none', 'pending', 'approved', 'rejected', 'withdrawn'],
'pbl_visibility': ['private', 'class', 'school', 'public_readonly'],
'pbl_evidence_type': ['artifact', 'process_event', 'reflection', 'feedback'],
'pbl_agent_type': ['designer', 'critic'],
'pbl_tool_state': ['enabled', 'disabled_contract_only'],
'pbl_artifact_type': ['document', 'presentation', 'model', 'code', 'media'],
'pbl_event_type': ['mission.completed', 'task.completed', 'pbl.unlockMission',
'entity.state.changed', 'session.member.joined', 'artifact.submitted'],
}
W = []
def w(path, content):
full = os.path.join(os.path.dirname(path) and path.rsplit(os.sep, 1)[0], '')
os.makedirs(os.path.dirname(path), exist_ok=True)
old = None
if os.path.exists(path):
with open(path, encoding='utf-8') as f:
old = f.read()
if old != content:
with open(path, 'w', encoding='utf-8') as f:
f.write(content)
W.append(os.path.relpath(path, ROOT))
def ddl_of(name):
spec = T[name]
cols = [' `tenant_id` %s NOT NULL COMMENT %s' % (TYPE_MAP['tenant'], '租户ID(强制打头)'),
' `id` %s COMMENT %s' % (TYPE_MAP['pk'], '主键')]
for f in spec['fields']:
fn, ft, cmt = f[0], f[1], f[2]
nn = f[3] if len(f) > 3 else ft not in NULLABLE
cols.append(' `%s` %s%s COMMENT %s' % (fn, TYPE_MAP[ft], '' if nn else ' NULL',
json.dumps(cmt, ensure_ascii=False)))
# 时间列一律 DATETIME不使用 时间戳类型),值由应用层 pbl_common.api.now_str() 写入,
# 避免隐式默认带来的时区漂移;默认值为纪元占位(非 NULL便于排序与增量拉取
cols.append(' `created_at` DATETIME NOT NULL DEFAULT \'1970-01-01 00:00:00\' COMMENT %s'
% json.dumps('创建时间(应用层写入)', ensure_ascii=False))
cols.append(' `updated_at` DATETIME NOT NULL DEFAULT \'1970-01-01 00:00:00\' COMMENT %s'
% json.dumps('更新时间(应用层写入)', ensure_ascii=False))
cols.append(' PRIMARY KEY (`id`)')
for iname, icols, uniq in spec['indexes']:
cols.append(' %s `%s` (%s)' % ('UNIQUE KEY' if uniq else 'KEY', iname,
', '.join('`%s`' % c for c in icols)))
body = ',\n'.join(cols)
sql = 'CREATE TABLE IF NOT EXISTS `%s` (\n%s\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4' % (name, body)
sql += " COLLATE=utf8mb4_general_ci COMMENT=%s" % json.dumps(spec['comment'], ensure_ascii=False)
if spec['partition_month']:
parts = ',\n'.join(" PARTITION p%s VALUES LESS THAN ('%s')" % (y, '%d-01-01' % y)
for y in range(2026, 2032))
sql += (',\n KEY `idx_re_part` (`occurred_at`)\nPARTITION BY RANGE COLUMNS(`occurred_at`)'
' (\n%s\n PARTITION pmax VALUES LESS THAN (MAXVALUE)\n )' % parts)
return sql + ';\n'
def model_json(name):
spec = T[name]
fields = [{'name': 'tenant_id', 'comment': '租户ID(强制打头)', 'null': False, **ABSTRACT['tenant']},
{'name': 'id', 'comment': '主键', 'null': False, **ABSTRACT['pk']}]
for f in spec['fields']:
fn, ft, cmt = f[0], f[1], f[2]
nn = f[3] if len(f) > 3 else ft not in NULLABLE
fields.append({'name': fn, 'comment': cmt, 'null': not nn, **ABSTRACT[ft]})
fields.append({'name': 'created_at', 'comment': '创建时间', 'null': False, **ABSTRACT['tsnn']})
fields.append({'name': 'updated_at', 'comment': '更新时间', 'null': False, **ABSTRACT['tsnn']})
doc = {
'summary': [{'name': name, 'comment': spec['comment'], 'module': spec['module'],
'engine': 'mariadb', 'charset': 'utf8mb4', 'tenant_scoped': True}],
'fields': fields,
'indexes': [{'name': i[0], 'unique': i[2], 'fields': i[1]} for i in spec['indexes']]
+ [{'name': 'PRIMARY', 'unique': True, 'fields': ['id']}],
'codes': [{'field': c[0], 'appcode': c[1]} for c in spec['codes']],
}
return json.dumps(doc, ensure_ascii=False, indent=2) + '\n'
def crud_json(mod, name):
spec = T[name]
br, ed = {}, {}
for f in spec['fields']:
fn, ft, cmt = f[0], f[1], f[2]
ui = {'label': cmt or fn, 'type': 'text' if ft in ('txt', 'js', 'sid', 'sref') else
'number' if ft in ('int', 'bigint', 'dec', 'ref') else
'checkbox' if ft == 'bool' else 'date' if ft.startswith('ts') else 'select'}
if ui['type'] == 'date':
ui['type'] = 'text'
br[fn] = dict(ui, list=True)
if ft != 'pk':
ed[fn] = ui
br = dict({'id': {'label': 'ID', 'list': True, 'type': 'number'},
'tenant_id': {'label': '租户ID', 'list': True, 'type': 'text'}}, **br)
doc = {'tblname': name, 'alias': mod, 'browserfields': br,
'params': {'browserfields': br, 'editable': ed,
'order_by': 'id DESC', 'page_size': 20}}
return json.dumps(doc, ensure_ascii=False, indent=2) + '\n'
DSPY = """# {mod}/api/{fn}.dspy —— 契约端点(自动生成,勿手改:改 spec 后跑 gen_artifacts.py
debug('{mod}/api/{fn}.dspy: START params_kw={{dict(params_kw)}}')
data = await {fn}(**params_kw)
return data
"""
INIT_PY = '''#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""`load_{mod}()` —— {mod} 模块唯一挂载入口(自动生成)。
注册三处同步之 env.<契约名> = <契约名> 定义在 api.py 导出在 __init__.py
"""
from ahserver.serverenv import ServerEnv
from {mod}.api import (
{imports}
)
def load_{mod}():
env = ServerEnv()
env.get_module_dbname = env.get_module_dbname # 库名由应用决定,模块不硬编码
{registers}
return '{mod}'
'''
DYN_INIT_PY = '''#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""`load_{mod}()` —— {mod} 模块唯一挂载入口。
注册三处同步之 env.<契约名> = <契约名> 定义在 api.py 导出在 __init__.py
"""
from ahserver.serverenv import ServerEnv
from {mod}.api import (
{imports}
)
def load_{mod}():
env = ServerEnv()
{registers}
return '{mod}'
'''
PKG_INIT_PY = '''#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""{mod} —— {desc}
注册三处同步之 必须导出 init.py 里的全部契约函数漏一行 .dspy 调用即 NameError
"""
from {mod}.init import load_{mod}
from {mod}.api import (
{imports}
)
__all__ = [
'load_{mod}',
{exports}
]
'''
PYPROJECT = '''[project]
name = "{mod}"
version = "0.1.0"
description = "{desc}"
requires-python = ">=3.9"
dependencies = ["apppublic", "sqlor", "ahserver", "appbase", "rbac"]
[build-system]
requires = ["setuptools>=61"]
build-backend = "setuptools.build_meta"
[tool.setuptools]
packages = ["{mod}"]
'''
LOAD_PATH = '''#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""{mod} RBAC 路径注册(硬门禁 6.6 / QC #11
约定
- 路径 = 模块自动路由 `/{mod}/api/<契约>.dspy`不带端口不带 /wss 前缀
- 角色 `logined` = 登录即可访问的读接口写接口按角色分级teacher/admin
- apps/pbls/build.sh 8 步调用 `register()`rbac CLI 不在位时打印清单不静默跳过
"""
import os
import subprocess
import sys
MODULE = '{mod}'
# (path, role)
PATHS = [
{entries}
]
def register():
tool = os.environ.get('RBAC_SET_PERM', 'set_role_perm.py')
done, missing = 0, []
for path, role in PATHS:
if subprocess.call([sys.executable if os.environ.get('PY') else 'python3',
tool, role, path],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) == 0:
done += 1
else:
missing.append((path, role))
print('[%s] rbac paths: total=%%d ok=%%d pending=%%d' %% (len(PATHS), done, len(missing)))
for path, role in missing:
print(' PENDING %%-12s %%s' %% (role, path))
return len(missing) == 0
if __name__ == '__main__':
sys.exit(0 if register() else 1)
'''
SKILL_MD = '''# {mod} 模块技能(自动生成骨架 + 人工补充)
## 定位
{desc}
## 挂载
`from {mod}.init import load_{mod}` `load_{mod}()`应用 app/pbls.py init() 中按序调用
## 数据表({tn} 张)
{tables}
## 契约接口({cn} 个,路径 `/{mod}/api/<name>.dspy`
{contracts}
## 陷阱
- 库名一律 `ServerEnv().get_module_dbname('{mod}')`禁止硬编码 DBNAME
- sqlor 只有 `C/U/D/R/I/sqlExe`查询走 pbl_common.api q_all/q_one已适配
- 所有读写强制带 `tenant_id`pbl_common.api.tenant_id()缺失即 fail-closed 报错
- 新增契约需同步三处api.py 定义 + __init__.py 导出 + init.py env 注册 + scripts/load_path.py 路径
'''
INDEX_UI = json.dumps({
"widgettype": "VBox",
"options": {"width": "100%", "height": "100%", "padding": "20px"},
"subwidgets": [
{"widgettype": "Text", "options": {"label": "{title}", "fontSize": "24px"}},
{"widgettype": "ResponsableBox", "options": {"gap": "16px", "minWidth": "250px"},
"subwidgets": []},
{"widgettype": "VBox", "id": "{mod}_content",
"options": {"width": "100%", "flex": "1", "marginTop": "20px"}}]
}, ensure_ascii=False, indent=2) + '\n'
def contracts_of(mod):
return MODULES[mod]['contracts']
def write_module(mod, desc):
spec = MODULES[mod]
base = os.path.join(MODS, mod)
pkg = os.path.join(base, mod)
cs = contracts_of(mod)
imports = ''.join(" %s,\n" % c for c in cs)
registers = ''.join(" env.%s = %s\n" % (c, c) for c in cs)
exports = ''.join(" %r,\n" % c for c in cs)
w(os.path.join(pkg, 'init.py'), DYN_INIT_PY.format(
mod=mod, imports=imports, registers=registers))
w(os.path.join(pkg, '__init__.py'), PKG_INIT_PY.format(
mod=mod, desc=desc, imports=imports, exports=exports))
w(os.path.join(base, 'pyproject.toml'), PYPROJECT.format(mod=mod, desc=desc))
entries = ''.join(" ('/%s/api/%s.dspy', %r),\n" % (mod, c, 'logined') for c in cs)
w(os.path.join(base, 'scripts', 'load_path.py'), LOAD_PATH.format(
mod=mod, entries=entries))
w(os.path.join(base, 'skill', 'SKILL.md'), SKILL_MD.format(
mod=mod, desc=desc, tn=len(spec['tables']),
tables='\n'.join('- `%s`%s' % (t, T[t]['comment']) for t in spec['tables']) or '- 无(零表模块)',
cn=len(cs), contracts='\n'.join('- `%s`' % c for c in cs)))
cards = []
for c in cs:
cards.append({
"widgettype": "VBox",
"options": {"backgroundColor": "#FFFFFF", "padding": "20px", "cursor": "pointer"},
"binds": [{"wid": "self", "event": "click", "actiontype": "urlwidget",
"target": "app.%s_content" % mod,
"options": {"url": "{{entire_url('api/%s.dspy')}}" % c}, "mode": "replace"}],
"subwidgets": [{"widgettype": "Text", "options": {"label": c}}]})
ui = json.loads(INDEX_UI.replace('{title}', desc).replace('{mod}', mod))
ui['subwidgets'][1]['subwidgets'] = cards
w(os.path.join(base, 'wwwroot', 'index.ui'),
json.dumps(ui, ensure_ascii=False, indent=2) + '\n')
for c in cs:
w(os.path.join(base, 'wwwroot', 'api', c + '.dspy'), DSPY.format(mod=mod, fn=c))
for t in spec['tables']:
w(os.path.join(base, 'models', t + '.json'), model_json(t))
w(os.path.join(base, 'json', '%s_%s.json' % (mod[4:], t)), crud_json(mod, t))
DESCS = {
'pbl_common': 'PBL 公共内核(租户上下文/DB 适配/错误码/审计/CRUD 工厂)',
'pbl_appcodes': 'PBL 枚举编码注入6+2 组,幂等,零表)',
'pbl_blueprint': 'PBL 蓝图聚合根与子对象、版本、模板M1a/M1b',
'pbl_validation': 'PBL 校验引擎14 维 + 5 级质量状态M2',
'pbl_compiler': 'PBL Compiler v1确定性编译 + Game DefinitionM3a/M3b',
'pbl_agent_runtime': 'Designer/Critic Agent 运行时与 fail-closed 工具裁决M4a/M4b',
'pbl_evidence': '产出物与证据幂等采集M5a/M5b',
'pbl_assessment': 'Rubric 加权评估与报告M6',
'pbl_kdb_ext': 'KDB 只读桩与匿名聚合M7零写入',
'pbl_domain_ext': 'world/scene/entity 薄扩展:租户/班级/团队关联M8不改基表',
'pbl_scense_ext': 'scense/scense_game 前端覆盖层与页面注册M9',
'pbl_runtime_ext': 'scense_runtime 薄扩展:单事务事件+状态写入与广播M11a/M11b',
}
for m in MODULES:
write_module(m, DESCS[m])
# ── 应用级 DDL36 表,按模块顺序)──────────────────────────────────────────
order = ['pbl_blueprint', 'pbl_validation', 'pbl_compiler', 'pbl_agent_runtime',
'pbl_evidence', 'pbl_assessment', 'pbl_kdb_ext', 'pbl_domain_ext',
'pbl_scense_ext', 'pbl_runtime_ext']
parts = ["-- pbls 36 表 DDLMariaDB 10.x 方言,由 scripts/gen_artifacts.py 从同一 spec 生成)",
"-- 硬约束tenant_id 打头 / id 为 BIGINT UNSIGNED AUTO_INCREMENT 主键;"
"-- 不使用 外键约束 / 枚举列类型 / 时间戳列类型 / 位图列类型(枚举值走 appcodes 编码表,"
"时间列走 DATETIME 由应用层写入)",
"-- 方言来源projects/pbls/env/test.json -> db.engine = mariadb"
"(禁 BIGSERIAL/SERIAL/nextval 等 PG 语法)",
"SET NAMES utf8mb4;", ""]
n = 0
for mod in order:
parts.append('-- ===== %s (%s) =====' % (mod, MODULES[mod]['no']))
for t in MODULES[mod]['tables']:
parts.append(ddl_of(t))
n += 1
parts.append('-- ===== 枚举 6+2 组由 pbl_appcodes.load_pbl_appcodes() 幂等注入 appcodes本文件不含 INSERT =====')
w(os.path.join(APP, 'scripts', 'ddl', 'pbls_tables.sql'), '\n'.join(parts) + '\n')
assert n == 36, n
# 分模块 DDL 片段(模块仓库自包含,便于单模块重建表)
for mod in order:
frag = ['-- %s 表 DDL自动生成与 apps/pbls/scripts/ddl/pbls_tables.sql 同源)' % mod]
for t in MODULES[mod]['tables']:
frag.append(ddl_of(t))
w(os.path.join(MODS, mod, 'sql', mod + '.sql'), '\n'.join(frag) + '\n')
# ── appcodes 种子数据pbl_appcodes 幂等注入源)─────────────────────────────
w(os.path.join(MODS, 'pbl_appcodes', 'init', 'data.json'),
json.dumps({'appcodes': APPCODES}, ensure_ascii=False, indent=2) + '\n')
print('generated/updated %d files, tables=%d, contracts=%d, appcodes=%d groups'
% (len(W), len(T), sum(len(v['contracts']) for v in MODULES.values()), len(APPCODES)))
for p in W:
print(' +', p)

View File

@ -0,0 +1,654 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""pbls 离线契约冒烟测试(无 DB / 无网络,用内存 FakeDB 驱动真实 api.py 代码路径)。
覆盖硬门禁与关键红线
A 租户强制 + CRUD 白名单丢弃越权字段
B 14 维校验 + 5 级质量状态阶梯 blocking 不放行
C 确定性编译draft 门禁拒 同输入两次指纹一致 改版本指纹变 幂等命中
D 证据幂等 (source_event_id, evidence_type) 二次采集 deduped不产生第二行
E fail-closed 8 步裁决Critic / 禁用工具(pbl.publish) / 未注册 / 参数白名单 / 审批缺失
F Rubric 权重合计100% 拒绝=100% 通过且 total_score 派生
G KDB 零写入 + 匿名聚合 k=5 抑制 + 个体维度拒绝
H 运行时事件 seq/state_version 单调 + 乐观锁 + 8 人上限 + 时延预算实测
I appcodes 幂等注入二次执行 inserted=0
J 入口挂载静态核验ServerEnv 先于 load_rbac + 契约三处同步 + 12 loader 可导入
K G6 十一条禁止实现零命中
L 前端覆盖层 12 页面 + 渲染时延预算可执行验证方式
"""
import asyncio
import os
import re
import sys
import types
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.abspath(os.path.join(HERE, *(['..'] * 4)))
for _m in sorted(os.listdir(os.path.join(ROOT, 'modules'))):
if _m.startswith('pbl_'):
sys.path.insert(0, os.path.join(ROOT, 'modules', _m))
sys.path.insert(0, os.path.join(ROOT, 'apps', 'pbls', 'app'))
# ── FakeDB支持 INSERT/UPDATE/DELETE/SELECT(等值+字面量+LIKE+>)/COUNT/MAX/GROUP BY ──
class FakeSor:
def __init__(self, db):
self.db = db
async def sqlExe(self, sql, params=None):
p = dict(params or {})
s = ' '.join(sql.split())
self.db.log.append(s[:160])
up = s.upper()
if up.startswith('INSERT'):
return self._insert(s, p)
if up.startswith('UPDATE'):
return self._update(s, p)
if up.startswith('DELETE'):
return self._delete(s, p)
return self._select(s, p, up)
# ---- INSERT ----
def _insert(self, s, p):
tbl = re.search(r'INSERT INTO `(\w+)`', s).group(1)
colpart = s[s.index('(') + 1:s.index(')')]
cols = re.findall(r'`(\w+)`', colpart)
vals = s[s.upper().index('VALUES') + 6:]
ph = re.findall(r'\$\{(\w+)\}\$', vals)
self.db.next_id += 1
row = {}
for i, c in enumerate(cols):
row[c] = p.get(ph[i]) if i < len(ph) else None
row['id'] = self.db.next_id
self.db.rows.setdefault(tbl, []).append(row)
return [{'id': self.db.next_id}]
# ---- UPDATE ----
def _update(self, s, p):
tbl = re.search(r'UPDATE `(\w+)`', s).group(1)
where = s[s.upper().index(' WHERE ') + 7:] if ' WHERE ' in s.upper() else ''
setpart = s[s.upper().index(' SET ') + 5: s.upper().index(' WHERE ')] if ' WHERE ' in s.upper() \
else s[s.upper().index(' SET ') + 5:]
sets = re.findall(r'`(\w+)`\s*=\s*\$\{(\w+)\}\$', setpart)
n = 0
for row in self.db.rows.get(tbl, []):
if not self._match(row, where, p):
continue
for col, key in sets:
row[col] = p.get(key)
if '`use_count` = `use_count` + 1' in setpart:
row['use_count'] = int(row.get('use_count') or 0) + 1
n += 1
return [{'affected': n}]
# ---- DELETE ----
def _delete(self, s, p):
tbl = re.search(r'DELETE FROM `(\w+)`', s).group(1)
where = s[s.upper().index(' WHERE ') + 7:] if ' WHERE ' in s.upper() else ''
keep, n = [], 0
for row in self.db.rows.get(tbl, []):
if self._match(row, where, p):
n += 1
else:
keep.append(row)
self.db.rows[tbl] = keep
return [{'affected': n}]
# ---- SELECT ----
def _select(self, s, p, up):
if 'LAST_INSERT_ID' in up:
return [{'pk': self.db.next_id}]
m = re.search(r'FROM `(\w+)`', s)
tbl = m.group(1) if m else '?'
rows = self.db.rows.get(tbl, [])
where = ''
if ' WHERE ' in up:
w = s[s.upper().index(' WHERE ') + 7:]
where = re.split(r' ORDER BY | GROUP BY | LIMIT ', w)[0]
matched = [r for r in rows if self._match(r, where, p)]
gm = re.search(r'GROUP BY `(\w+)`', s)
if gm:
col = gm.group(1)
groups = {}
for r in matched:
groups.setdefault(r.get(col), []).append(r)
out = []
for g, items in sorted(groups.items(), key=lambda x: str(x[0])):
scores = [float(i.get('total_score') or 0) for i in items]
out.append({'g': g, 'n': len(items),
'avg_score': sum(scores) / len(scores) if scores else 0,
'min_score': min(scores) if scores else 0,
'max_score': max(scores) if scores else 0})
return out
if 'COUNT(*)' in up:
return [{'c': len(matched)}]
m = re.search(r'COALESCE\((MAX|COUNT)\(`(\w+)`\),0\) AS (\w+)', s)
if m:
fn, col, alias = m.groups()
vals = [r.get(col) for r in matched if r.get(col) is not None]
if fn == 'COUNT':
return [{alias: len(matched)}]
try:
return [{alias: max(vals) if vals else 0}]
except TypeError:
return [{alias: 0}]
m = re.search(r'MAX\(`(\w+)`\) AS (\w+)', s)
if m:
col, alias = m.groups()
vals = [r.get(col) for r in matched if r.get(col) is not None]
try:
return [{alias: max(vals) if vals else 0}]
except TypeError:
return [{alias: 0}]
if 'DISTINCT' in up:
col = re.search(r'DISTINCT `(\w+)`', s).group(1)
seen = []
for r in matched:
if r.get(col) is not None and r[col] not in seen:
seen.append(r[col])
return [{col: v} for v in seen]
lim = re.search(r'LIMIT (\d+)', s)
if lim:
matched = matched[:int(lim.group(1))]
return [dict(r) for r in matched]
def _match(self, row, where, p):
if not where:
return True
for cond in re.split(r' AND ', where):
cond = cond.strip().strip('()')
m = re.match(r'`(\w+)`\s*=\s*\$\{(\w+)\}\$$?', cond)
if m:
col, key = m.groups()
want = p.get(key)
if want in (None, ''):
continue
if str(row.get(col)) != str(want):
return False
continue
m = re.match(r'`(\w+)`\s*=\s*\'([^\']*)\'$', cond)
if m:
col, want = m.groups()
if str(row.get(col)) != want:
return False
continue
m = re.match(r'`(\w+)`\s*>\s*\$\{(\w+)\}\$$?', cond)
if m:
col, key = m.groups()
try:
if float(row.get(col) or 0) <= float(p.get(key) or 0):
return False
except (TypeError, ValueError):
return False
continue
m = re.match(r'`(\w+)` IN \(([^)]*)\)$', cond)
if m:
col, keys = m.group(1), re.findall(r'\$\{(\w+)\}', m.group(2))
vals = [p.get(k) for k in keys if p.get(k) not in (None, '')]
if vals and str(row.get(col)) not in [str(v) for v in vals]:
return False
continue
if 'LIKE' in cond.upper():
m = re.match(r'`(\w+)` LIKE \$\{(\w+)\}\$$?', cond)
if m:
col, key = m.groups()
pat = str(p.get(key) or '').replace('%', '')
if pat and pat not in str(row.get(col) or ''):
return False
continue
return True
class FakeCtx:
def __init__(self, db, name):
self.db, self.name = db, name
async def __aenter__(self):
return FakeSor(self.db)
async def __aexit__(self, *a):
return False
class FakeDBMod:
def __init__(self):
self.rows, self.log, self.next_id = {}, [], 0
def sqlorContext(self, name):
return FakeCtx(self, name)
fdb = FakeDBMod()
_ap = types.ModuleType('apppublic')
_ap.db = fdb
sys.modules['apppublic'] = _ap
ah = types.ModuleType('ahserver')
se = types.ModuleType('ahserver.serverenv')
class ServerEnv:
_inst = None
def __new__(cls):
if cls._inst is None:
cls._inst = object.__new__(cls)
return cls._inst
def __init__(self):
self.pbl_default_tenant = 'T-DEMO'
se.ServerEnv = ServerEnv
ah.serverenv = se
sys.modules['ahserver'] = ah
sys.modules['ahserver.serverenv'] = se
_ge = types.ModuleType('ahserver.globalEnv')
_ge.password_encode = lambda s: s
sys.modules['ahserver.globalEnv'] = _ge
_wa = types.ModuleType('ahserver.webapp')
_wa.webapp = lambda init: None
sys.modules['ahserver.webapp'] = _wa
RESULTS = []
def check(name, cond, detail=''):
RESULTS.append((name, bool(cond), detail))
print('%-4s %s %s' % ('PASS' if cond else 'FAIL', name, detail))
async def main():
from pbl_common import api as C
from pbl_blueprint import api as BP
from pbl_validation import api as VA
from pbl_compiler import api as CO
from pbl_agent_runtime import api as AR
from pbl_evidence import api as EV
from pbl_assessment import api as AS
from pbl_kdb_ext import api as KD
from pbl_runtime_ext import api as RT
from pbl_appcodes import api as AC
from pbl_scense_ext import api as SX
from pbl_domain_ext import api as DX
# ── A 租户 + CRUD 白名单 ────────────────────────────────────────────
r = await BP.pbl_blueprint_create(title='湿地调查', subject='science', grade='g5',
evil_column='x')
check('A1 蓝图创建返回 id', r.get('ok') and r.get('id'), str(r.get('id')))
check('A2 越权字段被丢弃不入库', r.get('dropped_fields') == ['evil_column'],
str(r.get('dropped_fields')))
check('A3 租户上下文注入', r.get('tenant_id') == 'T-DEMO', str(r.get('tenant_id')))
rd = await BP.pbl_blueprint_read(id=r['id'])
check('A4 聚合读含 7 类子对象槽', set(rd['subobjects']) == {
'learning_goal', 'role', 'mission', 'task', 'artifact_spec', 'evidence_spec',
'reflection_spec'}, str(sorted(rd['subobjects'])))
# ── B 14 维校验 + 5 级质量状态 ─────────────────────────────────────
for i, (code, title) in enumerate([('LG1', '理解生态链'), ('LG2', '数据建模'),
('LG3', '公众表达')]):
await BP.pbl_blueprint_subobject_save(kind='learning_goal', blueprint_id=r['id'],
code=code, title=title, bloom_level='analyze',
measurable=1, order_no=i + 1)
await BP.pbl_blueprint_update(id=r['id'], content_json={
'schema': 'pbl.blueprint.v1', 'driving_question': '我们如何拯救社区湿地?',
'scenario': '本地湿地富营养化,社区需要一份可执行方案',
'learning_goals': [{'uid': 'LG1', 'code': 'LG1'}, {'uid': 'LG2', 'code': 'LG2'},
{'uid': 'LG3', 'code': 'LG3'}],
'rubric_criteria': [{'code': 'C1', 'learning_goal_id': 'LG1', 'weight': 40},
{'code': 'C2', 'learning_goal_id': 'LG2', 'weight': 35},
{'code': 'C3', 'learning_goal_id': 'LG3', 'weight': 25}]})
await BP.pbl_blueprint_subobject_save(kind='mission', blueprint_id=r['id'], code='MS1',
title='拯救湿地', scenario_txt='富营养化现场',
order_no=1)
await BP.pbl_blueprint_subobject_save(kind='role', blueprint_id=r['id'], code='R1',
title='生态调研员', headcount=3, order_no=1)
await BP.pbl_blueprint_subobject_save(kind='role', blueprint_id=r['id'], code='R2',
title='数据分析师', headcount=3, order_no=2)
await BP.pbl_blueprint_subobject_save(kind='artifact_spec', blueprint_id=r['id'], code='A1',
title='治理方案', artifact_type='document', order_no=1)
await BP.pbl_blueprint_subobject_save(kind='evidence_spec', blueprint_id=r['id'],
code='E1', evidence_type='artifact', min_count=2,
rubric_criterion_uid='C1', order_no=1)
await BP.pbl_blueprint_subobject_save(kind='evidence_spec', blueprint_id=r['id'],
code='E2', evidence_type='reflection', min_count=1,
rubric_criterion_uid='C2', order_no=2)
await BP.pbl_blueprint_subobject_save(kind='reflection_spec', blueprint_id=r['id'],
code='RF1', trigger='mission.completed',
prompt_txt='你学到了什么?', min_words=50,
evidence_type='reflection', order_no=1)
v = await VA.pbl_validation_run(blueprint_id=r['id'], apply_state=1)
check('B1 校验 14 维全覆盖', v['dimension_count'] == 14, str(v['dimension_count']))
check('B2 逐维可解释结论齐备', len(v['findings']) == 14 and all(
'detail' in f for f in v['findings']), str(len(v['findings'])))
check('B3 质量状态在 5 级阶梯内', v['quality_state'] in
['draft', 'needs_work', 'pbl_ready', 'playtest_ready', 'publish_ready'],
'%s score=%s' % (v['quality_state'], v['score']))
check('B4 校验时延预算 ≤2000ms 实测', v['within_budget'], '%sms' % v['latency_ms'])
check('B5 blocking 未清零不得跳级', not (v['blocking'] > 0 and
v['quality_state'] == 'publish_ready'),
'blocking=%s' % v['blocking'])
vfull = await VA.pbl_validation_run(blueprint_id=r['id'])
check('B6 重复校验产生新记录(历史不覆盖)', vfull['ok'], 'idempotency=n/a')
# ── C 确定性编译 ────────────────────────────────────────────────────
await CO.pbl_capability_register(capability_key='pbl.mission.show', category='mission',
is_enabled=1)
await CO.pbl_capability_register(capability_key='pbl.artifact.submit', category='artifact',
is_enabled=1)
try:
await CO.pbl_compiler_compile(blueprint_id=r['id'])
check('C0 draft/未达门禁编译被拒', False, '未抛错')
except C.PblError as e:
check('C0 质量门禁拒编译', e.code == 'PBL_COMPILE_GATE_FAILED', e.code)
await BP.pbl_blueprint_version_create(blueprint_id=r['id'])
c1 = await CO.pbl_compiler_compile(blueprint_id=r['id'], min_quality_state='draft')
c2 = await CO.pbl_compiler_compile(blueprint_id=r['id'], min_quality_state='draft')
check('C1 编译产出 SHA-256 指纹', len(str(c1.get('content_fingerprint'))) == 64,
str(c1.get('content_fingerprint'))[:16])
check('C2 同输入+同编译版本 → 同指纹29.6',
c1['content_fingerprint'] == c2['content_fingerprint'],
'%s ==' % c1['content_fingerprint'][:12])
check('C2b 同指纹命中幂等(不重复建行)', c2.get('idempotent') is True,
str(c2.get('note'))[:20])
pv = await CO.pbl_compiler_preview(blueprint_id=r['id'])
check('C3 干跑不落库且指纹一致', pv['content_fingerprint'] == c1['content_fingerprint']
and pv['preview'], 'entities=%s' % len(pv['definition']['entities']))
check('C3b 事件映射为 script_type=1 规则 JSON不建规则引擎',
all(b['script_type'] == 1 for b in pv['definition']['script_bindings'])
and len(pv['definition']['script_bindings']) == len(pv['definition']['events']),
'bindings=%s' % len(pv['definition']['script_bindings']))
await BP.pbl_blueprint_update(id=r['id'], content_json={'schema': 'pbl.blueprint.v1',
'missions': [{'obj_uid': 'M9'}]})
await BP.pbl_blueprint_version_create(blueprint_id=r['id'])
c3 = await CO.pbl_compiler_compile(blueprint_id=r['id'], blueprint_version_no=2,
min_quality_state='draft', force_new=1)
check('C4 改输入 → 指纹变(非缓存假阳性)',
c3['content_fingerprint'] != c1['content_fingerprint'], 'v2 fp 不同')
cmp_ = await CO.pbl_compiler_compare(a=c1['id'], b=c3['id'])
check('C5 两版定义结构化 diff 可用', cmp_['ok'] and cmp_['identical'] is False,
'added=%s removed=%s' % (len(cmp_['diff']['added']), len(cmp_['diff']['removed'])))
check('C6 编译时延预算 ≤5000ms 实测', c3['latency_ms'] <= c3['budget_ms'],
'%sms' % c3['latency_ms'])
mat = await DX.pbl_domain_materialize_game_definition(id=c3['id'])
check('C7 物化走基座契约/降级如实回报', mat['ok'] and 'degraded' in mat and
mat.get('base_tables_altered') is False, 'degraded=%s' % mat.get('degraded'))
# ── E fail-closed 8 步裁决 ──────────────────────────────────────────
await AR.pbl_tool_registry_save(tool_key='pbl.get', title='读蓝图', enabled=1,
agent_scope='both')
await AR.pbl_tool_registry_save(tool_key='pbl.update', title='改蓝图', enabled=1,
agent_scope='designer',
params_schema_json={'properties': {'blueprint_id': {},
'content_json': {}}})
await AR.pbl_tool_registry_save(tool_key='pbl.publish', title='发布', enabled=0,
agent_scope='none', disabled_reason='G6_no_publish_tool')
from pbl_common.api import sql_exec as _x
for code, mode in (('designer', 'write'), ('critic', 'read')):
await _x('INSERT INTO `pbl_agent_def` (`tenant_id`,`code`,`agent_type`,`name`,'
'`model_route`,`permission_mode`,`is_enabled`,`version_no`,`created_at`)'
' VALUES (${t}$,${c}$,${a}$,${n}$,${r}$,${m}$,${en}$,${vn}$,${ts}$)',
{'t': 'T-DEMO', 'c': code, 'a': code, 'n': code, 'r': 'pipeline-llm',
'm': mode, 'en': 1, 'vn': 1, 'ts': '2026-09-15 10:00:00'}, 'pbl')
d1 = await AR.pbl_tool_adjudicate(agent_code='critic', tool_key='pbl.update',
params_json={'blueprint_id': 1})
check('E1 Critic 写工具 DENIED零写权限',
d1['allowed'] is False and d1['error_code'] == 'AD04_PERMISSION_MODE_DENIED',
d1['error_code'])
d1b = await AR.pbl_tool_adjudicate(agent_code='critic', tool_key='pbl.get', params_json={})
check('E1b Critic 读工具放行', d1b['allowed'] is True, str(d1b['steps'][-1]))
d2 = await AR.pbl_tool_adjudicate(agent_code='designer', tool_key='pbl.publish',
params_json={})
check('E2 pbl.publish 恒拒G6 无绕过)',
d2['allowed'] is False and d2['error_code'] == 'AD03_TOOL_DISABLED', d2['error_code'])
d3 = await AR.pbl_tool_adjudicate(agent_code='designer', tool_key='evil.tool', params_json={})
check('E3 未注册工具 DENIED', d3['error_code'] == 'AD02_TOOL_UNREGISTERED', d3['error_code'])
d4 = await AR.pbl_tool_adjudicate(agent_code='designer', tool_key='pbl.update',
params_json={'evil': 1})
check('E4 参数不在服务端白名单 DENIED', d4['error_code'] == 'AD05_PARAM_NOT_WHITELISTED',
d4['error_code'])
d5 = await AR.pbl_tool_adjudicate(agent_code='designer', tool_key='pbl.update',
params_json={'blueprint_id': 1})
check('E5 需人工审批且无 approved 单 → DENIED',
d5['error_code'] == 'AD06_APPROVAL_REQUIRED', d5['error_code'])
ap = await AR.pbl_approval_create(approval_type='modify_learning_goal',
target_type='pbl_blueprint', target_id=r['id'])
check('E5b 审批单创建为 pending', ap.get('ok') and ap.get('approval_uid'),
str(ap.get('approval_uid'))[:12])
dec = await AR.pbl_approval_decide(id=ap['id'], status='approved', comment_txt='同意')
check('E5c 人工决策后放行第6步通过', dec.get('ok') is True, dec.get('status'))
try:
await AR.pbl_approval_create(approval_type='auto_publish', target_id=1)
check('E6 非法审批类型被拒', False, '未抛错')
except C.PblError as e:
check('E6 非法审批类型被拒', e.code == 'PBL_APPROVAL_TYPE_UNKNOWN', e.code)
try:
await AR.pbl_tool_registry_save(tool_key='sim.configure', enabled=1)
check('E7 G6 禁用工具不得启用', False, '未抛错')
except C.PblError as e:
check('E7 G6 禁用工具不得启用', e.code == 'PBL_TOOL_FORBIDDEN', e.code)
tl = await AR.pbl_tool_registry_list()
check('E8 13 启用 / 9 禁用清单齐备',
len(tl['enabled_keys']) == 13 and len(tl['disabled_keys']) == 9
and 'pbl.publish' in tl['disabled_keys'], str(tl['counts']))
cr = await AR.pbl_agent_critic_run(blueprint_id=r['id'])
check('E9 Critic 输出四要素且 writable=False',
cr['writable'] is False and all(k in cr for k in
('recommendation', 'reason', 'evidence',
'confidence')), 'conf=%s' % cr['confidence'])
dr = await AR.pbl_agent_designer_run(input_txt='给我做一个湿地 PBL', subject='science',
grade='g5', template_code='TPL-NOT-EXIST')
check('E10 LLM 不可达 → 兜底路径闭环不中断',
dr['ok'] and dr['generation_source'] == 'template_fallback',
'err=%s %sms' % (dr['llm_error'], dr['latency_ms']))
tr = await AR.pbl_agent_trace_list()
check('E11 轨迹留痕7 要素)可读', tr['ok'] and tr['total'] >= 2, 'traces=%s' % tr['total'])
# ── D 证据幂等 ──────────────────────────────────────────────────────
a1 = await EV.pbl_artifact_create(title='湿地报告', session_id=1, blueprint_id=r['id'],
content_json={'a': 1})
e1 = await EV.pbl_evidence_collect(evidence_type='artifact', source_event_id='EV-1',
session_id=1, learner_id='U1', artifact_id=a1['id'])
e2 = await EV.pbl_evidence_collect(evidence_type='artifact', source_event_id='EV-1',
session_id=1, learner_id='U1', artifact_id=a1['id'])
check('D1 首次采集落库', e1.get('deduped') is False, str(e1.get('id')))
check('D2 重复采集幂等(唯一索引)', e2.get('deduped') is True and
e2.get('id') == e1.get('id'), '%s==%s' % (e1.get('id'), e2.get('id')))
check('D2b 幂等键为 (tenant,source_event_id,evidence_type)',
'uk_ev_dedup' in e1.get('unique_index', ''), e1.get('unique_index'))
try:
await EV.pbl_evidence_collect(evidence_type='artifact')
check('D3 缺 source_event_id 拒绝', False, '未抛错')
except C.PblError as e:
check('D3 缺 source_event_id 拒绝', e.code == 'PBL_PARAM_MISSING', e.code)
try:
await EV.pbl_evidence_collect(evidence_type='ghost', source_event_id='X')
check('D3b 非法证据类型拒绝', False, '未抛错')
except C.PblError as e:
check('D3b 非法证据类型拒绝', e.code == 'PBL_EVIDENCE_TYPE_INVALID', e.code)
au = await EV.pbl_artifact_update(id=a1['id'], content_json={'a': 2})
check('D4 产出物更新版本递增', au.get('ok'), 'v=%s' % au.get('version_no'))
el = await EV.pbl_evidence_list(session_id=1, learner_id='U1')
check('D5 证据按类型聚合可读', el['ok'] and 'by_type' in el, str(el['by_type']))
# ── F Rubric ────────────────────────────────────────────────────────
try:
await AS.pbl_rubric_save(title='湿地评分', blueprint_id=r['id'], criteria_json=[
{'code': 'C1', 'weight': 60, 'learning_goal_id': 'LG1', 'max_points': 5}])
check('F1 权重≠100% 拒绝', False, '未抛错')
except C.PblError as e:
check('F1 权重≠100% 拒绝', e.code == 'PBL_RUBRIC_WEIGHT_INVALID', e.code)
try:
await AS.pbl_rubric_save(title='x', blueprint_id=r['id'], criteria_json=[
{'code': 'C1', 'weight': 100}])
check('F1b 准则缺 learning_goal_id 拒绝', False, '未抛错')
except C.PblError as e:
check('F1b 准则缺 learning_goal_id 拒绝',
e.code == 'PBL_RUBRIC_ALIGNMENT_MISSING', e.code)
ru = await AS.pbl_rubric_save(title='湿地评分', blueprint_id=r['id'], criteria_json=[
{'code': 'C1', 'weight': 60, 'learning_goal_id': 'LG1', 'max_points': 5},
{'code': 'C2', 'weight': 40, 'learning_goal_id': 'LG2', 'max_points': 5}])
check('F2 权重=100% 通过', ru.get('ok') and ru.get('weight_total_ok'), str(ru.get('id')))
sc = await AS.pbl_assessment_score(rubric_id=ru['id'], learner_id='U1',
scores_json={'C1': 4, 'C2': 3})
check('F3 加权总分派生4/5×60 + 3/5×40 = 72', abs(sc['total_score'] - 72.0) < 0.01,
'%s band=%s' % (sc['total_score'], sc['band']))
check('F3b 客户端不可直传 total_score', 'total_score' not in
[k for k in sc.keys() if k == 'total_score'] or sc.get('derived'),
str(sc.get('derived'))[:24])
rep = await AS.pbl_assessment_report(learner_id='U1')
check('F4 教师报告可读', rep.get('ok') and 'learners' in rep, str(rep.get('record_count')))
# ── G KDB 只读桩 ────────────────────────────────────────────────────
ks = await KD.pbl_kdb_search(keyword='湿地')
check('G1 KDB 检索为只读桩', ks.get('writable') is False and 'stub' in ks, ks.get('stub'))
check('G2 pbl_kdb_item 零 INSERT',
not any('INSERT INTO `pbl_kdb_item`' in x for x in fdb.log), 'log clean')
ag = await KD.pbl_research_aggregate(group_by='band')
check('G3 匿名聚合只读 + k_min=5', ag.get('anonymous') and ag.get('written') is False
and ag.get('k_min') == 5, 'groups=%s suppressed=%s' % (len(ag['groups']),
ag['suppressed_groups']))
try:
await KD.pbl_research_aggregate(group_by='learner_id')
check('G4 个体维度聚合被拒', False, '未抛错')
except C.PblError as e:
check('G4 个体维度聚合被拒', e.code == 'PBL_AGG_DIM_INVALID', e.code)
check('G5 无 kdb.write/research.write/sim.* 实现入口',
not any(hasattr(KD, n) for n in ('kdb_write', 'pbl_kdb_write', 'pbl_research_write',
'pbl_sim_configure', 'pbl_kdb_add_candidate')),
'零实现入口')
# ── H 运行时(服务端权威)───────────────────────────────────────────
m1 = await RT.pbl_session_member_add(session_id=1, user_id='U1')
m2 = await RT.pbl_session_member_add(session_id=1, user_id='U1')
check('H1 成员重复加入幂等', m2.get('deduped') is True, str(m2.get('members')))
st = await RT.pbl_entity_state_apply(session_id=1, entity_id='mission:M1',
state_json={'completed': True},
event_type='mission.completed', state_version=999)
check('H2 客户端 state_version 被忽略', st.get('client_version_ignored') is True
and st['state_version'] == 1, 'v=%s' % st['state_version'])
st2 = await RT.pbl_entity_state_apply(session_id=1, entity_id='mission:M1',
state_json={'x': 1})
check('H3 state_version 单调递增', st2['state_version'] == 2, str(st2['state_version']))
check('H4 单事务 + 裁决/端到端时延实测在预算内', st2['within_budget'] is True
and st2['transaction'] == 'single', json.dumps(st2['measured']))
check('H4b 广播时延预算 ≤300ms 实测', st2['broadcast']['within_budget'],
'%sms ch=%s' % (st2['broadcast']['latency_ms'], st2['broadcast']['channel']))
pl = await RT.pbl_runtime_event_poll(session_id=1, since_seq=0)
check('H5 3s 轮询兜底可拉增量事件', pl['count'] >= 1 and pl['last_seq'] >= 1,
'events=%s interval=%ss' % (pl['count'], pl['poll_interval_seconds']))
try:
await RT.pbl_entity_state_apply(session_id=1, entity_id='mission:M1',
state_json={'y': 2}, if_state_version=1)
check('H6 乐观锁冲突被拒', False, '未抛错')
except C.PblError as e:
check('H6 乐观锁冲突被拒', e.code == 'PBL_STATE_CONFLICT', e.code)
for i in range(2, 9):
await RT.pbl_session_member_add(session_id=1, user_id='U%d' % i)
try:
await RT.pbl_session_member_add(session_id=1, user_id='U99')
check('H7 超 8 人上限被拒', False, '未抛错')
except C.PblError as e:
check('H7 超 8 人上限被拒', e.code == 'PBL_SESSION_FULL', e.code)
check('H8 事件表无 UPDATE/DELETE 出口append-only',
not any(x.startswith('UPDATE `pbl_runtime_event`') or
x.startswith('DELETE FROM `pbl_runtime_event`') for x in fdb.log), 'clean')
# ── I appcodes 幂等 ─────────────────────────────────────────────────
i1 = await AC.pbl_appcodes_inject()
i2 = await AC.pbl_appcodes_inject()
check('I1 6+2 组枚举注入', i1['groups'] == 8 and i1['inserted'] > 0,
'inserted=%s' % i1['inserted'])
check('I2 二次执行零新增(幂等)', i2['inserted'] == 0 and i2['skipped'] == i1['inserted'],
'skipped=%s' % i2['skipped'])
cl = await AC.pbl_appcodes_list()
check('I3 枚举组可读回', len(cl['groups']) >= 1, str(sorted(cl['groups'])[:3]))
# ── J 入口挂载静态核验 ──────────────────────────────────────────────
src = open(os.path.join(ROOT, 'apps', 'pbls', 'app', 'pbls.py'), encoding='utf-8').read()
i_env, i_rbac = src.index('env = ServerEnv()'), src.index('load_rbac()')
check('J1 ServerEnv() 先于 load_rbac()', 0 < i_env < i_rbac,
'env@%d < rbac@%d' % (i_env, i_rbac))
check('J2 基础模块显式 loader 调用',
all(x in src for x in ('load_appbase()', 'load_rbac()', 'load_pybricks()')), 'ok')
check('J2b 无 importlib 只 import 即标 loaded 的反模式',
'importlib' not in src or 'getattr(pkg' in src, 'ok')
bad = []
mods = ('pbl_common', 'pbl_appcodes', 'pbl_blueprint', 'pbl_validation', 'pbl_compiler',
'pbl_agent_runtime', 'pbl_evidence', 'pbl_assessment', 'pbl_kdb_ext',
'pbl_domain_ext', 'pbl_scense_ext', 'pbl_runtime_ext')
for mod in mods:
pkg = os.path.join(ROOT, 'modules', mod, mod)
init_src = open(os.path.join(pkg, 'init.py'), encoding='utf-8').read()
exp_src = open(os.path.join(pkg, '__init__.py'), encoding='utf-8').read()
api_src = open(os.path.join(pkg, 'api.py'), encoding='utf-8').read()
for n in re.findall(r'async def (pbl_\w+)', api_src):
if ('env.%s = %s' % (n, n)) not in init_src or n not in exp_src:
bad.append('%s:%s' % (mod, n))
check('J3 契约三处同步(定义+导出+env注册', not bad, '缺=%s' % (bad[:5] or ''))
check('J4 12 模块 load_* 均可导入',
all(hasattr(__import__('importlib').import_module('%s.init' % m), 'load_%s' % m)
for m in mods), 'ok')
check('J5 模块嵌套包结构 modules/{m}/{m}/',
all(os.path.isfile(os.path.join(ROOT, 'modules', m, m, 'api.py')) for m in mods), 'ok')
check('J6 模块无 app.py / 无独立端口(模块非部署单元)',
not any(os.path.exists(os.path.join(ROOT, 'modules', m, 'app.py')) or
os.path.exists(os.path.join(ROOT, 'modules', m, 'app', '%s.py' % m))
for m in mods), 'ok')
# ── K G6 十一条禁止实现 ─────────────────────────────────────────────
g6 = ['pbl_publish', 'marketplace', 'showplace_publish', 'sim_configure',
'kdb_add_candidate', 'kdb_propose_pattern', 'research_write', 'crdt',
'frame_sync', 'unity', 'sso_billing', 'autonomous_agent']
hits = {}
for term in g6:
n = 0
for mod in mods:
pkg = os.path.join(ROOT, 'modules', mod, mod)
for fn in os.listdir(pkg):
if not fn.endswith('.py'):
continue
t = open(os.path.join(pkg, fn), encoding='utf-8').read()
n += len(re.findall(r'(?:async def|def)\s+\w*%s\w*\s*\(' % term, t, re.I))
hits[term] = n
check('K1 G6 十一条禁止实现零命中(无函数定义)',
all(v == 0 for v in hits.values()), str({k: v for k, v in hits.items() if v}) or 'all 0')
# ── L 前端覆盖层 ────────────────────────────────────────────────────
pr = await SX.pbl_page_registry_list()
check('L1 12 页面注册齐备', pr['page_count'] == 12, str(pr['page_count']))
world = [p for p in pr['data'] if p['widget_type'] == 'world_overlay' or
p['page_key'] in ('pbl_play_preview', 'pbl_mission_board', 'pbl_role_panel',
'pbl_artifact_studio', 'pbl_evidence_wall')]
check('L2 世界内页面预算 500ms', all(int(p['budget_ms']) == 500 for p in world),
'%s pages' % len(world))
check('L2b 首屏预算 1500ms', pr['budgets']['first_screen_ms'] == 1500,
json.dumps(pr['budgets']))
bs = await SX.pbl_overlay_bootstrap(session_id=1)
check('L3 覆盖层一次请求拿齐上下文 + 时延实测',
bs['ok'] and len(bs['pages']) == 12 and bs['within_budget'],
'%sms budget=%sms' % (bs['latency_ms'], bs['budget_ms']))
fb = await SX.pbl_playtest_feedback_save(session_id=1, blueprint_id=r['id'],
feedback_type='reflection',
content_txt='我学会了用数据说服社区', rating=4)
check('L4 反思写入并联动证据链', fb.get('ok'), str(fb.get('id')))
try:
await SX.pbl_playtest_feedback_save(feedback_type='ghost')
check('L4b 非法反馈类型被拒', False, '未抛错')
except C.PblError as e:
check('L4b 非法反馈类型被拒', e.code == 'PBL_FEEDBACK_TYPE_INVALID', e.code)
ctx = await SX.pbl_session_context_get(session_id=1)
check('L5 会话上下文只读authority=server', ctx['authority'] == 'server'
and ctx['max_members'] == 8, 'members=%s' % ctx['count'])
fails = [x for x in RESULTS if not x[1]]
print('\n==== 结果:%d 项,通过 %d,失败 %d ====' % (len(RESULTS), len(RESULTS) - len(fails),
len(fails)))
for name, _ok, detail in fails:
print(' FAIL', name, detail)
return 1 if fails else 0
if __name__ == '__main__':
sys.exit(asyncio.get_event_loop().run_until_complete(main()))

View File

@ -0,0 +1,225 @@
/*
* pbl_overlay.js PBL 前端覆盖层M9
* 挂载方式 scense 渲染容器之上叠加 DOM 覆盖层 + 事件处理器
* 不修改 scense/scense_game 渲染主循环render loop untouched
* 性能承诺广播渲染 <= 500mspoll/subscribe 增量应用首屏 <= 1.5s骨架先行 + 懒加载数据
* 页面12 页面page_code 路由组件11 错误降级态9
*/
(function (global) {
'use strict';
var API = '/api/pbls'; // 应用唯一入口 :9300
var POLL_MS = 3000; // 3s 轮询兜底(与后端 POLL_FALLBACK_MS 一致)
var BROADCAST_BUDGET_MS = 500; // 广播渲染预算
var FIRST_SCREEN_BUDGET_MS = 1500; // 首屏预算
// 12 页面ui-design.md
var PAGES = ['blueprint_list', 'blueprint_editor', 'goal_editor', 'scene_editor',
'step_editor', 'rule_editor', 'validation_report', 'compile_artifacts',
'agent_chat', 'approval_center', 'play_launcher', 'assessment_report'];
// 11 组件(覆盖层内,不改渲染层)
var COMPONENTS = ['TopBar', 'NavRail', 'GoalCard', 'SceneCard', 'StepList', 'RubricEditor',
'ValidationBadge', 'ArtifactViewer', 'ChatPanel', 'ApprovalQueue', 'HudOverlay'];
// 9 类错误降级态
var ERROR_STATES = {
NET_FAIL: ['网络中断', '显示缓存数据 + 重试按钮'],
AUTH_FAIL: ['登录失效', '跳转登录,保留草稿于 localStorage'],
LOAD_FAIL: ['数据加载失败', '骨架占位 + 局部重试'],
VALIDATE_BLOCK: ['质量门禁未过', '阻断编译按钮,跳转校验报告'],
APPROVAL_PENDING: ['待人工审批', '按钮置灰 + 审批单号提示'],
LLM_FALLBACK: ['LLM 不可用', '离线模板兜底提示US-13'],
RATE_LIMIT: ['请求过于频繁', '退避倒计时后自动重试'],
SESSION_ENDED: ['会话已结束', '只读回放模式'],
RENDER_DEGRADED: ['渲染降级', '覆盖层保留2D 占位图']
};
function PblOverlay(opts) {
this.root = opts.root; // 覆盖层容器scense 容器之上)
this.playSessionId = opts.playSessionId || null;
this.participantId = opts.participantId || null;
this.tenantId = opts.tenantId;
this.afterSeq = 0;
this.timer = null;
this.state = {};
this.mount();
}
PblOverlay.prototype.mount = function () {
var t0 = performance.now();
this.root.className = 'pbl-overlay';
this.root.innerHTML =
'<div class="pbl-topbar" data-c="TopBar"></div>' +
'<div class="pbl-navrail" data-c="NavRail"></div>' +
'<div class="pbl-main" data-page="blueprint_list"></div>' +
'<div class="pbl-hud" data-c="HudOverlay"></div>';
this.bindEvents();
this.loadFirstScreen().then(function () {
var cost = performance.now() - t0;
if (cost > FIRST_SCREEN_BUDGET_MS) {
global.console.warn('[pbl_overlay] first screen %.0fms > %.0fms budget',
cost, FIRST_SCREEN_BUDGET_MS);
}
});
};
/* 事件处理器:只监听/派发,不介入渲染主循环 */
PblOverlay.prototype.bindEvents = function () {
var self = this;
this.root.addEventListener('click', function (e) {
var act = e.target.getAttribute('data-act');
if (!act) return;
var handlers = {
'send-command': function (el) { self.sendCommand(el); },
'retry': function () { self.pollOnce(); },
'goto-approval': function () { self.showPage('approval_center'); },
'goto-validation': function () { self.showPage('validation_report'); }
};
if (handlers[act]) handlers[act](e.target);
});
// scense 侧事件桥scense 通过 CustomEvent 通知,覆盖层被动响应)
if (global.addEventListener) {
global.addEventListener('scense:entity-clicked', function (e) {
self.onEntityClicked(e.detail);
});
global.addEventListener('scense:ready', function () { self.degrade('RENDER_DEGRADED', false); });
}
};
PblOverlay.prototype.loadFirstScreen = function () {
var self = this;
return this.api('play.list', { blueprint_id: self.blueprintId }).then(function (r) {
self.state.plays = (r.data && r.data.items) || [];
self.renderHud();
}).catch(function () { self.degrade('LOAD_FAIL', true); });
};
/* 服务端权威命令client_seq 幂等state_version 由服务端维护,客户端不传不校验 */
PblOverlay.prototype.sendCommand = function (el) {
var self = this;
this._seq = (this._seq || 0) + 1;
var body = {
play_session_id: this.playSessionId,
actor_id: this.participantId,
client_seq: 'c-' + (this.participantId || 'anon') + '-' + this._seq,
entity_id: el.getAttribute('data-entity'),
capability: el.getAttribute('data-cap'),
command: el.getAttribute('data-cmd'),
state_delta: JSON.parse(el.getAttribute('data-delta') || '{}')
};
return this.api('runtime.command', body).then(function (r) {
var d = r.data || {};
if (d.ack_result === 'DENIED') {
self.toast('操作被拒绝: ' + (d.reason || ''), true);
} else if (d.ack_result === 'ACCEPTED') {
self.afterSeq = Math.max(self.afterSeq, d.event_seq || 0);
}
return d;
}).catch(function (e) {
self.degrade(String(e.code || 'NET_FAIL'), true);
});
};
PblOverlay.prototype.onEntityClicked = function (detail) {
if (!detail) return;
this.api('runtime.state_read', {
play_session_id: this.playSessionId, entity_id: detail.entityId
}).then(function (r) {
global.dispatchEvent(new CustomEvent('pbl:entity-state', { detail: r.data }));
}).catch(function () { });
};
/* 增量拉取并渲染:预算 500ms超时降级为下一次轮询 */
PblOverlay.prototype.pollOnce = function () {
if (!this.playSessionId) return;
var self = this, t0 = performance.now();
this.api('runtime.poll', { play_session_id: this.playSessionId, after_seq: this.afterSeq })
.then(function (r) {
var d = r.data || {};
(d.events || []).forEach(function (ev) {
self.afterSeq = Math.max(self.afterSeq, ev.event_seq);
if (performance.now() - t0 > BROADCAST_BUDGET_MS) { return; }
global.dispatchEvent(new CustomEvent('pbl:broadcast', { detail: ev }));
});
self.state.states = d.states || self.state.states;
self.renderHud();
})
.catch(function () { self.degrade('NET_FAIL', true); });
};
PblOverlay.prototype.startPolling = function () {
var self = this;
this.stopPolling();
this.timer = setInterval(function () { self.pollOnce(); }, POLL_MS);
};
PblOverlay.prototype.stopPolling = function () {
if (this.timer) { clearInterval(this.timer); this.timer = null; }
};
PblOverlay.prototype.renderHud = function () {
var hud = this.root.querySelector('.pbl-hud');
if (!hud) return;
var goals = (this.state.goalProgress || []);
hud.innerHTML = goals.map(function (g) {
return '<div class="pbl-goal">' + escapeHtml(g.goal_code) + ' : ' + g.percent + '%</div>';
}).join('');
};
PblOverlay.prototype.showPage = function (code) {
if (PAGES.indexOf(code) < 0) { return; }
var main = this.root.querySelector('.pbl-main');
main.setAttribute('data-page', code);
this.api('overlay.save', {
play_session_id: this.playSessionId, participant_id: this.participantId,
page_code: code, overlay: { visited_at: Date.now() }
}).catch(function () { });
};
PblOverlay.prototype.degrade = function (key, on) {
var info = ERROR_STATES[key];
if (!info) return;
var box = this.root.querySelector('.pbl-error') || document.createElement('div');
if (on) {
box.className = 'pbl-error';
box.innerHTML = '<b>' + info[0] + '</b><span>' + info[1] + '</span>' +
'<button data-act="retry">重试</button>';
if (!box.parentNode) this.root.appendChild(box);
} else if (box.parentNode) {
box.parentNode.removeChild(box);
}
};
PblOverlay.prototype.toast = function (msg, isErr) {
var t = document.createElement('div');
t.className = 'pbl-toast' + (isErr ? ' pbl-toast-err' : '');
t.textContent = msg;
this.root.appendChild(t);
setTimeout(function () { if (t.parentNode) t.parentNode.removeChild(t); }, 3000);
};
PblOverlay.prototype.api = function (contract, body) {
var self = this;
return fetch(API + '/' + contract, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-Tenant-Id': this.tenantId || '' },
credentials: 'include',
body: JSON.stringify(body || {})
}).then(function (r) {
if (r.status === 401) { self.degrade('AUTH_FAIL', true); throw { code: 'AUTH_FAIL' }; }
if (r.status === 429) { self.degrade('RATE_LIMIT', true); throw { code: 'RATE_LIMIT' }; }
return r.json();
});
};
function escapeHtml(s) {
return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c];
});
}
global.PblOverlay = PblOverlay;
global.PBL_PAGES = PAGES;
global.PBL_COMPONENTS = COMPONENTS;
global.PBL_ERROR_STATES = ERROR_STATES;
})(window);