pbl_compiler —— PBL Compiler v1(确定性编译 + Game Definition)
模块仓库:
modules/pbl_compiler/| 里程碑:M3a(编译核心与确定性产物)+ M3b(版本登记/对比) 挂载方式:宿主应用apps/pbls/app/pbls.py的init()中按序调用load_pbl_compiler()。 本模块是 Python 包(业务模块):无app.py、无独立端口、无 Dockerfile、无模块级build.sh—— 它只因宿主应用调用load_pbl_compiler()而运行(module-development-spec 铁律)。 本文档为磁盘真实正文(非占位、非元描述、非自我指涉)。文中所有目录树 / 端点表 / 表字段 / 断言组均以modules/pbl_compiler/磁盘实测为准,不含磁盘不存在的文件(历史退回项,见 §8 P2/P3/P8)。 与skill/SKILL.md双向同步,同步规则见 §9。
1. 模块定位
pbl_compiler 是 PBL 产线的编译层:把 pbl_blueprint 的蓝图版本快照(含 7 类子对象)编译为运行时可直接消费的
Game Definition(GD),并保证「同输入必得同输出」的确定性(determinism,需求 29.6 / US-11)。
M3a 承担 6 项核心职责:
-
确定性规范化(canonicalization)——
pbl_compiler/canonical.py任意嵌套 JSON → 唯一规范串 →sha256指纹。规则固定:sort_keys=True、零空白分隔符(',', ':')、ensure_ascii=False、UTF-8 编码、浮点定点化、拒绝 NaN/Infinity;编译前统一strip_volatile()剥离易变字段(created_at/updated_at/duration_ms/task_no/id等),使指纹只反映业务内容。 -
Game Definition 构建——
pbl_compiler/gd_builder.pybuild_game_definition(snapshot, ctx, registry_rows)是纯函数(零 LLM、零 IO、零随机), 产出固定 10 个顶层键的 GD 文档(键清单见 §7.2),并返回 64 位content_fingerprint。 键序、缺省值、数组排序全部显式决定,禁止依赖 dict 插入顺序或数据库返回行序。 -
能力注册表指纹(registry_hash) GD 编译时把所用能力注册表快照纳入
manifest.registryHash并参与content_fingerprint: 能力集变更(增删 /version_no升级 /args_schema变更)→registry_hash变更 → 指纹变更, 避免「蓝图没改但语义已改」的假幂等。口径铁律见 §8 P1(历史两轮退回根因位,本轮已统一并加断言守护)。 -
编译任务与审批门禁——
api.py: compile_blueprint()/ 契约pbl_compiler_compile走 ①-⑧ 全流程(§7.1),fail-closed:未审批、质量状态未达门禁、蓝图/版本缺失一律拒绝编译, 错误码PBL_E_*回写任务并落审计,绝不产出半成品 GD。 -
指纹幂等落库
pbl_game_definition上UNIQUE KEY uk_gd_fp (tenant_id, content_fingerprint); 同租户同指纹重编译命中即复用(返回既有game_def_id+idempotent=true),不产生重复行、不产生新 id。 -
编译器版本与对比(M3b)
pbl_compiler_version锁定编译器版本(code/semver/rules_hash/entrypoint),version_register / version_save / version_get / version_list / version_diff / compare提供登记、保存、取回、 谱系列表与结构化差异对比;pbl_compiler_verify_determinism是 US-11 / F-CP-03「同输入重编译指纹相等」的验收唯一入口。
非目标(本模块不做):不做蓝图 CRUD(属 pbl_blueprint);不做 14 维校验规则实现(属 pbl_validation,
本模块只消费其 quality_state 结论作门禁);不做 Agent 推理与工具裁决(属 pbl_agent_runtime);
不做前端游戏渲染(属 pbl_scense_ext / scense);不做证据采集(属 pbl_evidence)。
2. 数据表(4 张:models/*.json 四段式定义 + sql/pbl_compiler.sql DDL)
所有表首列 tenant_id(强制打头),所有读写 SQL 必带 tenant_id 条件;缺失租户上下文由
pbl_common.api.tenant_id() 直接 fail-closed 抛错,不做「默认租户」兜底。
created_at / updated_at / started_at / finished_at / duration_ms / task_no / id 均为审计或易变字段,
一律被 strip_volatile 排除,不参与指纹(否则幂等彻底失效,见 §8 P7)。
2.1 pbl_game_definition(编译产物 GD,核心表)
| 字段 | 类型 | 用途 |
|---|---|---|
| tenant_id | str(32) NOT NULL | 租户隔离,参与唯一约束 |
| id | int(20) PK AUTO_INCREMENT | GD 主键(不参与指纹) |
| blueprint_id | int(20) NOT NULL | 来源蓝图 |
| blueprint_version_no | int NOT NULL | 来源蓝图版本(输入锁定) |
| compiler_version_id | int(20) NOT NULL | 编译器版本(pbl_compiler_version.id,算法变更即换版,防跨版本假幂等) |
| content_fingerprint | str(64) NOT NULL | sha256(canonical_json(strip_volatile(GD))),US-11 比对锚点 |
| definition_json | text(LONGTEXT) | GD 完整正文(10 顶层键,规范串落库) |
| world_id / scene_id | int(20) | 落库到 world / scene 的关联 id(供 M9 渲染) |
| entity_count / event_count | int | GD 规模计数(manifest.counts 同源) |
| quality_state | str(64) NOT NULL | 编译时质量状态(来自 pbl_validation 5 级阶梯) |
| duration_ms | int | 编译耗时(易变,不参与指纹) |
| created_at / updated_at | datetime | 审计(应用层写入,不参与指纹) |
关键约束:UNIQUE KEY uk_gd_fp (tenant_id, content_fingerprint) —— 「确定性编译 + 幂等落库」的数据库级保证:
- 同租户、同内容 → 数据库层拒绝第二行 → 代码层先
SELECT ... WHERE tenant_id AND content_fingerprint命中即复用 (idempotent=true),并发下再兜唯一冲突 → 回读既有行,实现并发安全幂等; - 跨租户相同内容允许各存一份(租户隔离优先于全局去重);
- 表为 append-only:GD 不原地改写,重编译产生新指纹新行,旧行由
pbl_compiler_version谱系追溯。
其余索引:KEY idx_gd_bp (tenant_id, blueprint_id)、PRIMARY (id)。
2.2 pbl_compiler_version(编译器版本谱系,29.6 确定性锚点)
| 字段 | 类型 | 用途 |
|---|---|---|
| tenant_id | str(32) NOT NULL | 租户隔离,参与唯一约束 |
| id | int(20) PK | 版本记录 id |
| code | str(64) NOT NULL | 版本编码(如 pblc-v1) |
| semver | str(64) NOT NULL | 语义化版本 |
| rules_hash | str(32) NOT NULL | 规则集 SHA-256 前缀(manifest.rulesHash 来源) |
| entrypoint | str(64) NOT NULL | 编译入口函数(compile_blueprint) |
| enabled | bool NOT NULL | 是否启用(未启用版本不得用于新编译) |
| notes | text | 版本说明 / changelog |
| created_at / updated_at | datetime | 审计(不参与指纹) |
约束:UNIQUE KEY uk_cv_code (tenant_id, code)。编译器版本参与 GD manifest.compilerVersion:
编译算法或规则集一旦变更必须升版,否则新旧产物指纹不可比(假幂等)。
2.3 pbl_capability_registry(能力/组件注册表)
| 字段 | 类型 | 用途 |
|---|---|---|
| tenant_id | str(32) NOT NULL | 租户隔离,参与唯一约束 |
| id | int(20) PK | 主键 |
| capability_key | str(64) NOT NULL | 能力键(如 pbl.role.skill、pbl.item.consume) |
| category | str(64) NOT NULL | 能力分类(role / item / rule / event / ui …) |
| args_schema_json | json(LONGTEXT) | 能力参数 JSON Schema(编译期参数校验 + 派生 args_schema_hash) |
| permission_required | str(64) NOT NULL | 所需权限(M4b fail-closed 工具裁决消费) |
| is_enabled | bool NOT NULL | 启用状态(registry_hash 计算入参之一) |
| version_no | int NOT NULL | 能力版本(升版 → registry_hash 变) |
| description | text | 说明 |
| created_at / updated_at | datetime | 审计(不参与指纹) |
约束:UNIQUE KEY uk_cap_key (tenant_id, capability_key, version_no)。
编译时对能力集合按 capability_key 排序 → 规范化(capability_key/version_no/args_schema_hash/
permission_required/is_enabled 五元组)→ sha256 得 registry_hash,写入 GD manifest.registryHash。
2.4 pbl_compile_task(编译任务与审计载体,F-CP-01 / US-10)
| 字段 | 类型 | 用途 |
|---|---|---|
| tenant_id | str(64) NOT NULL | 租户隔离,参与唯一约束 |
| id | int(20) PK | 主键 |
| task_no | str(64) NOT NULL | 任务流水号(确定性派生 CT+blueprint_id+version+seq,非 uuid) |
| blueprint_id / blueprint_version | int(20) / int(11) | 编译对象(版本须已审批) |
| compiler_version / ruleset_version | str(32) | 编译器与规则集版本(锁定) |
| status | str(32) | appcodes pbl_compile_status:pending/running/success/failed |
| approval_id | int(20) | 关联审批记录(未审批拒绝,F-CP-01) |
| game_def_id | int(20) | 产出的 GD id(success 时回填) |
| content_fingerprint | str(64) | 产物 SHA-256 指纹(success 时回填,US-11 比对锚点) |
| error_code / error_msg | str(64) / str(2048) | 失败错误码(PBL_E_*)与原因(fail-closed 可追溯) |
| triggered_by | str(64) | 触发人(审计) |
| duration_ms | int(11) | 耗时(易变,不参与指纹) |
| started_at / finished_at / created_at / updated_at | datetime | 审计(不参与指纹) |
约束/索引:UNIQUE KEY uk_ct_task_no (tenant_id, task_no)、KEY idx_ct_blueprint (tenant_id, blueprint_id, blueprint_version)、
KEY idx_ct_status (tenant_id, status)、PRIMARY (id)。
表定义规范:遵循 database-table-definition-spec 四段式(
summary/fields/indexes/codes), 抽象类型 + 整数length/dec,indexes[].fields为数组。CRUD 定义在json/:compiler_pbl_game_definition.json、compiler_pbl_compiler_version.json、compiler_pbl_capability_registry.json(pbl_compile_task为纯后端任务表,无 CRUD 页面,只经契约端点读写)。
3. 契约端点清单(15 个 .dspy,与磁盘逐行对应)
契约文件位于 wwwroot/api/*.dspy,磁盘实测 ls wwwroot/api/*.dspy | wc -l = 15,下表即磁盘清单
(无虚构项、无遗漏项;实现函数名 = 文件名去后缀,全部列入 api.py:__all__):
| # | 端点文件(wwwroot/api/) |
api.py 实现函数 | 方法 | 说明 |
|---|---|---|---|---|
| 1 | pbl_compiler_compile.dspy |
pbl_compiler_compile |
POST | 触发编译(§7.1 ①-⑧ 全流程,落库 + 审计 + 幂等命中) |
| 2 | pbl_compiler_preview.dspy |
pbl_compiler_preview |
POST | 试编译:只返回 GD + 指纹,不落库、不建任务 |
| 3 | pbl_compiler_compare.dspy |
pbl_compiler_compare |
POST | 两蓝图版本 / 两 GD 结构化对比 |
| 4 | pbl_compiler_verify_determinism.dspy |
pbl_compiler_verify_determinism |
POST | US-11 / F-CP-03 验收唯一入口:同输入重编译 N 次比对指纹 |
| 5 | pbl_compiler_task_get.dspy |
pbl_compiler_task_get |
GET | 编译任务详情(含 error_code/game_def_id/content_fingerprint) |
| 6 | pbl_compiler_task_list.dspy |
pbl_compiler_task_list |
GET | 任务分页列表(tenant_id 强制打头) |
| 7 | pbl_game_definition_get.dspy |
pbl_game_definition_get |
GET | 按 game_def_id 取 GD |
| 8 | pbl_game_definition_get_by_blueprint.dspy |
pbl_game_definition_get_by_blueprint |
GET | 按蓝图(+版本)取最新 / 指定 GD |
| 9 | pbl_compiler_version_register.dspy |
pbl_compiler_version_register |
POST | 登记编译器版本(写 pbl_compiler_version) |
| 10 | pbl_compiler_version_save.dspy |
pbl_compiler_version_save |
POST | 保存 / 更新版本记录(notes/enabled/rules_hash) |
| 11 | pbl_compiler_version_get.dspy |
pbl_compiler_version_get |
GET | 取单条版本记录 |
| 12 | pbl_compiler_version_list.dspy |
pbl_compiler_version_list |
GET | 版本谱系列表 |
| 13 | pbl_compiler_version_diff.dspy |
pbl_compiler_version_diff |
GET | 两版本结构化差异(键级 added/removed/changed) |
| 14 | pbl_capability_list.dspy |
pbl_capability_list |
GET | 能力注册表查询(含当前 registry_hash) |
| 15 | pbl_capability_register.dspy |
pbl_capability_register |
POST | 注册 / 升版能力(写 pbl_capability_registry) |
前端入口:wwwroot/index.ui(模块导航页,url 一律 {{entire_url('/pbl_compiler/...')}} 绝对路径,避免 RBAC 403)。
api.py 另导出内部函数(非 .dspy 契约,供模块内/其它模块经 ServerEnv 调用):
compile / compile_blueprint / get_compile_task / list_compile_tasks / get_game_definition /
get_game_definition_by_blueprint / verify_determinism / register_compiler_version / get_compiler_version /
list_compiler_versions / diff_compiler_versions / is_approved / ladder_at_or_above,
以及常量 COMPILER_VERSION / RULESET_VERSION / RULES_HASH_SEED / GD_SCHEMA / GD_TOP_KEYS /
QUALITY_LADDER / COMPILE_GATE_MIN_STATE(__all__ 共 35 项 = 15 契约 + 13 内部函数 + 7 常量)。
3.1 三处同步铁律
每个契约必须三处同步 + 一处 RBAC,缺一即路由不可达 / NameError: name 'xxx' is not defined(历史退回主因):
① pbl_compiler/api.py 定义函数并列入 __all__;
② pbl_compiler/__init__.py 导出(from .api import ...);
③ pbl_compiler/init.py 的 load_pbl_compiler() 内 env.<name> = <name> 注册;
④ scripts/load_path.py 的 PATHS 补该端点 RBAC 路径(禁止通配符 % / *,逐条显式;当前 15 条,角色 logined)。
3.2 自查命令(交付前必跑;②③④⑤ 输出必须为空,⑥ 不得抛异常,⑦ 必须输出 OK)
cd modules/pbl_compiler
# ① 磁盘端点数(期望 15)
ls wwwroot/api/*.dspy | wc -l
# ② __init__.py 是否全部导出(应无输出)
for f in $(ls wwwroot/api/*.dspy | xargs -n1 basename | sed 's/\.dspy$//'); do
grep -q "$f" pbl_compiler/__init__.py || echo "NOT_EXPORTED $f"; done
# ③ init.py 是否全部注册 env(应无输出)
for f in $(ls wwwroot/api/*.dspy | xargs -n1 basename | sed 's/\.dspy$//'); do
grep -q "$f" pbl_compiler/init.py || echo "NOT_REGISTERED $f"; done
# ④ 每个 .dspy 是否有实现(应无输出)
for f in $(ls wwwroot/api/*.dspy | xargs -n1 basename | sed 's/\.dspy$//'); do
grep -q "def $f" pbl_compiler/api.py || echo "NO_IMPL $f"; done
# ⑤ RBAC 路径覆盖每个端点(应无输出)
for f in $(ls wwwroot/api/*.dspy | xargs -n1 basename | sed 's/\.dspy$//'); do
grep -q "$f.dspy" scripts/load_path.py || echo "NO_RBAC $f"; done
# ⑥ RBAC register() 可跑通(不得抛 TypeError: not enough arguments for format string)
python3 -c "import sys;sys.path.insert(0,'scripts');import load_path;print(load_path.register())"
# ⑦ 禁硬编码 DB 名(应输出 OK no hardcoded db)
grep -rn "^DB *=\|DBNAME *= *'\|dbname *= *'pbl'" pbl_compiler/ --include='*.py' \
&& echo 'HARDCODE_FOUND' || echo 'OK no hardcoded db'
# ⑧ 语法编译(应输出 COMPILE_OK)
python3 -m py_compile pbl_compiler/*.py scripts/*.py && echo COMPILE_OK
# ⑨ 自检门禁(必须 exit=0)
python3 scripts/test_m3a_selfcheck.py ; echo "exit=$?"
4. 代码结构(磁盘实测目录树,find . -path ./.git -prune -o -type f -print)
modules/pbl_compiler/
├── README.md # 本文档(真实正文,10 章节 + 附录)
├── pyproject.toml # 包元数据:name = "pbl_compiler"
├── .gitignore
├── pbl_compiler/ # Python 包目录(= 模块名,非 src/)
│ ├── __init__.py # 导出全部契约 + load_pbl_compiler(三处同步之②)
│ ├── init.py # load_pbl_compiler():env 注册契约(三处同步之③)
│ ├── api.py # 15 契约 + 13 内部函数 + 7 常量(__all__ 35 项,三处同步之①)
│ ├── canonical.py # 确定性规范化 / 指纹 / registry_hash / stable_id(本模块地基)
│ └── gd_builder.py # 蓝图快照 → GD 10 顶层键(纯函数,含 GD_TOP_KEYS)
├── models/ # 4 张表定义(四段式 JSON)
│ ├── pbl_game_definition.json
│ ├── pbl_compiler_version.json
│ ├── pbl_capability_registry.json
│ └── pbl_compile_task.json
├── json/ # 3 份 CRUD 定义
│ ├── compiler_pbl_game_definition.json
│ ├── compiler_pbl_compiler_version.json
│ └── compiler_pbl_capability_registry.json
├── sql/
│ └── pbl_compiler.sql # 幂等建表 DDL(4 张表,CREATE TABLE IF NOT EXISTS)
├── wwwroot/
│ ├── index.ui # 模块入口导航页
│ └── api/*.dspy # 15 个契约端点(见 §3)
├── scripts/
│ ├── load_path.py # RBAC 路径注册(PATHS 15 条 + register())
│ └── test_m3a_selfcheck.py # M3a 自检(6 组断言 G1-G6,可独立运行,见 §6)
└── skill/
└── SKILL.md # 模块技能文档(与 README 双向同步,见 §9)
磁盘事实说明(防回归):本模块不含
app.py/ 端口配置 / Dockerfile / 模块级build.sh/compiler.py/versioning.py——编译主流程compile_blueprint()与版本逻辑实现在api.py内, GD 构建在gd_builder.py内。安装步骤由宿主应用apps/pbls/build.sh承担(§5.1)。 文档不得描述磁盘不存在的文件(历史退回项,见 §8 P8)。
4.1 canonical.py 确定性规范化要点(不可随意改,改则历史指纹全变)
canonical_json(obj):json.dumps(obj, sort_keys=True, separators=(',', ':'), ensure_ascii=False)—— 键排序 + 零空白 + 不转义非 ASCII,消除序列化歧义(缩进、键序、中文转义差异全部归零)。- 浮点定点化:
float按定点规则格式化(固定精度、去尾零),避免1.0/1.00000000001/ 平台差异导致指纹抖动; 整数不带小数点(1与1.0规范化结果稳定且互不混淆)。 - 拒绝 NaN / Infinity:
allow_nan=False,非法数值直接抛错而非产出NaN字面量(跨语言不可解析)。 strip_volatile(obj):递归(含list[dict]内部)剥离易变键(id/created_at/updated_at/duration_ms/task_no/started_at/finished_at/created_by/trace_id/nonce等),只剥不改业务值。- 数组稳定化:语义无序集合(能力集、标签集、子对象集合)按元素规范串排序后再序列化;
语义有序数组(场景步骤序列、事件时间线)保持原序,由
gd_builder显式决定哪些键属有序。 fingerprint(obj)=sha256(canonical_bytes(strip_volatile(obj))),输出 64 位小写十六进制。short_hash(text, length):定长前缀哈希(用于args_schema_hash、url_hash、rules_hash)。registry_hash(capabilities):对能力集合归一为{capability_key, version_no, args_schema_hash, permission_required, is_enabled}五元组 → 排序 → 规范化 →sha256。 入参兼容两种形态:原始注册表行(带args_schema_json)与 GD 内已派生的capabilities.items(带args_schema_hash)——派生值本身就是short_hash(args_schema_json, 16),故两种入参得到完全相同的哈希(§8 P1)。stable_id(blueprint_id, version_no, kind, seq):确定性 ID 派生{blueprint_id}:{version_no}:{kind}:{seq:05d}, GD 内一切 id 由输入派生,禁用 uuid / random(29.6「无随机」)。- 禁项:指纹路径上禁止
time.time()/datetime.now()/uuid/random/os.getpid()/ dict 迭代顺序依赖 / 无ORDER BY的 SQL 行序依赖。
5. 安装与集成
5.1 安装(由宿主应用 apps/pbls/build.sh 承担;模块自身无 build.sh)
# ① 安装模块包(模块仓库根目录)
cd modules/pbl_compiler && pip install .
# ② 建表(幂等 DDL;库名由宿主应用决定,勿写死)
mysql -h <db_host> -u <user> -p<pass> <dbname> < sql/pbl_compiler.sql
# ③ 前端链接:模块 wwwroot 软链到宿主应用 wwwroot(软链非 cp,保持同步)
ln -sf <abs>/modules/pbl_compiler/wwwroot <app>/wwwroot/pbl_compiler
# ④ RBAC 注册(显式 15 条路径,禁通配符)
python3 modules/pbl_compiler/scripts/load_path.py
# ⑤ 语法门禁 + 自检门禁(两者都必须通过才算安装成功)
python3 -m py_compile modules/pbl_compiler/pbl_compiler/*.py
python3 modules/pbl_compiler/scripts/test_m3a_selfcheck.py # 必须 exit=0
5.2 宿主应用挂载(apps/pbls/app/pbls.py)
from ahserver.webapp import webapp
from ahserver.serverenv import ServerEnv
from appbase.init import load_appbase
from rbac.init import load_rbac
from bricks_for_python.init import load_pybricks
from pbl_common.init import load_pbl_common
from pbl_blueprint.init import load_pbl_blueprint
from pbl_validation.init import load_pbl_validation
from pbl_compiler.init import load_pbl_compiler # ← 本模块唯一集成点
def get_module_dbname(m):
# 模块 → 库名映射集中在应用层;模块内禁止硬编码 DBNAME
return {'pbl_common': 'pbl', 'pbl_blueprint': 'pbl', 'pbl_validation': 'pbl',
'pbl_compiler': 'pbl'}.get(m, m)
def init():
env = ServerEnv() # 必须在所有 load_* 之前
env.get_module_dbname = get_module_dbname
load_appbase(); load_rbac(); load_pybricks()
load_pbl_common()
load_pbl_blueprint()
load_pbl_validation()
load_pbl_compiler() # 注册 15 契约 + 13 内部函数到 ServerEnv
if __name__ == '__main__':
webapp(init)
5.3 库名获取(禁硬编码 DB 名,历史退回项 #4)
# ✅ 正确(.py 模块文件内):从宿主注入的 ServerEnv 取
dbname = ServerEnv().get_module_dbname('pbl_compiler')
# ✅ 正确(.dspy 内):get_module_dbname 是 ServerEnv 注入的全局,无需 import、无需 ServerEnv()
dbname = get_module_dbname('pbl_compiler')
# ❌ 错误(已整改,禁止回归):
# DB = 'pbl' # 换库 / 多租户部署即断
# async with DBPools().sqlorContext('pbl') as sor: # 硬编码库名
api.py 中 sql_rows / sql_exec / tenant_crud 的库名一律来自 get_module_dbname('pbl_compiler')
或调用方注入的 db 参数;模块内不存在 DB = '...' / DBNAME = '...' 常量。自查命令见 §3.2 ⑦(实测输出 OK no hardcoded db)。
5.4 RBAC 注册(scripts/load_path.py)
PATHS 逐条列出 15 个契约路径(角色 logined)+ 模块目录 + index.ui,register() 逐条注册并返回统计。
格式串占位符个数必须与参数个数一致(历史 TypeError: not enough arguments for format string 已修,禁止回归):
print('[%s] rbac paths: total=%d ok=%d pending=%d' % (MODULE, len(PATHS), done, len(missing)))
print(' PENDING %-12s %s' % (role, path))
离线环境行为:无数据库 / 无 set_role_perm.py 依赖时,register() 不抛异常,逐条打印
PENDING <role> <path> 并返回 False(部署到真实环境后返回 True)。这是预期的 fail-safe 行为,
关键判据是「不得抛 TypeError」(§3.2 ⑥ 实测通过)。
5.5 建表 DDL(幂等,完整见 sql/pbl_compiler.sql)
CREATE TABLE IF NOT EXISTS `pbl_game_definition` (
`tenant_id` VARCHAR(32) 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(64) 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, `event_count` INT NOT NULL DEFAULT 0,
`quality_state` VARCHAR(64) NOT NULL COMMENT "编译时质量状态",
`duration_ms` INT NOT NULL DEFAULT 0,
`created_at` DATETIME NOT NULL, `updated_at` DATETIME NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_gd_fp` (`tenant_id`, `content_fingerprint`), -- ★ 确定性幂等的 DB 级保证
KEY `idx_gd_bp` (`tenant_id`, `blueprint_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 另 3 张:pbl_compiler_version(uk_cv_code) / pbl_capability_registry(uk_cap_key) / pbl_compile_task(uk_ct_task_no)
手工建表必须显式
COLLATE utf8mb4_unicode_ci,否则与json2ddl生成的表混用会报Illegal mix of collations。trigger是 MySQL 关键字,本模块用triggered_by列名规避。
6. 自检运行方法(scripts/test_m3a_selfcheck.py)
cd modules/pbl_compiler
python3 scripts/test_m3a_selfcheck.py ; echo "exit=$?"
- 输出逐条
PASS/FAIL明细 + 汇总--- summary: pass=N fail=M ---; - 退出码 0 = 6 组断言全部通过;1 = 有断言失败;2 = 被测模块加载失败(§5.1 ⑤ 安装门禁以此为准);
- 纯函数级自检:只按文件路径加载
canonical.py与gd_builder.py(importlib,绕开包__init__.py的 ahserver 依赖),不连数据库、不依赖宿主 ServerEnv,可在 CI 直接跑; - 防御式导入:被测函数缺失时打印
SELFCHECK_FAIL: missing <name>并计入 FAIL,绝不静默跳过 (历史退回项 R1:声称的自检脚本必须真实可跑)。
6 组断言(G1-G6,与脚本内 print('-- Gx ...') 标题一一对应)
| 组 | 脚本标题 | 断言要点 | 覆盖需求 |
|---|---|---|---|
| G1 | canonical 键序无关 / 指纹形态 |
深层嵌套 dict 键序打乱后规范串与指纹不变;指纹为 64 位小写 hex | 29.6 确定性 |
| G2 | 零空白 / ensure_ascii=False / 浮点定点 / NaN 拒绝 |
规范串无空格换行;中文不转义;1.0/1/1.00 各自稳定且互不混淆;NaN/Infinity 抛错 |
canonical 规则 |
| G3 | strip_volatile 易变字段不参与指纹 |
仅改 created_at/id/duration_ms/task_no 指纹不变;递归进入 list[dict];模块指纹 == sha256(参考规范化(strip_volatile(x)))(算法口径一致) |
幂等前提 / US-11 |
| G4 | Game Definition 10 顶层键 |
GD_TOP_KEYS == 10 键契约;build_game_definition 返回键数/键集一致;同时返回 64 位指纹且与 manifest.fingerprint 一致;manifest 声明 schema/deterministic=True/llmUsed=False(零 LLM);manifest.registryHash == canonical.registry_hash(GD capabilities.items)(统一口径);原始注册表能力全部进入 GD items 且 version_no 一致(派生不丢语义) |
F-CP-01 / F-CP-03(历史 FAIL 位) |
| G5 | 确定性:同输入重编译指纹全等 |
连续 3 次编译指纹全等;ctx 易变字段(created_at/duration_ms/task_no)变化 → 指纹不变;子对象入库顺序颠倒 → 指纹不变(集合稳定化排序生效);业务内容变更 → 指纹必变 |
US-11 / F-CP-03 |
| G6 | registry_hash 能力注册表指纹 |
64 位 sha256;与能力行顺序无关;新增能力 / version_no 升级 / args_schema 变更 → hash 变化;仅改 created_at/id → hash 不变;空注册表有确定哈希且与非空不同 |
F-CP-05 |
本轮实测结果(磁盘实跑,非声称):--- summary: pass=37 fail=0 ---,exit=0。
G4 口径修复说明(历史两轮退回根因):
gd_builder.build_capabilities()对注册表行补齐派生字段 (args_schema_hash/registered/used_by_gd/category)后以known.values()计算registry_hash, 而旧自检对原始注册表行计算 → 两侧口径永不相等 →exit=1、安装门禁自证失败。 本轮统一为「以 GD 内capabilities.items为唯一口径」,并让canonical.registry_hash()同时兼容 原始行(args_schema_json)与派生行(args_schema_hash)两种入参得到同值,另加「派生不丢语义」断言守护。
7. 编译主流程
7.1 compile_blueprint(blueprint_id, version_no, compiler_version, triggered_by, **kw) ①-⑧
(实现见 api.py,函数 docstring 与下表一致;契约入口 pbl_compiler_compile.dspy)
| 步 | 动作 | fail-closed 行为 |
|---|---|---|
| ① | 审批门禁(US-10 / F-CP-01):is_approved() 校验蓝图版本已审批,取 approval_id + 审批证据;同时校验 quality_state 达 COMPILE_GATE_MIN_STATE(第 15 章门禁阶梯 QUALITY_LADDER) |
未审批 → PBL_E_STATE_ILLEGAL(HTTP 403);质量未达标 → PBL_E_STATE_ILLEGAL(消息含 quality_state=%s 未达 %s)。不建任务、不落 GD |
| ② | 建编译任务:写 pbl_compile_task(status=running、approval_id、compiler_version、ruleset_version、task_no 确定性派生、triggered_by、started_at) |
写失败即中止,返回 task_no + error_code |
| ③ | 取蓝图版本快照(输入锁定):get_version() 读蓝图聚合根 + 7 类子对象,冻结为 snapshot,固定行序 |
蓝图/版本不存在 → status=failed + PBL_E_NOT_FOUND |
| ④ | 确定性转换生成 GD 10 顶层键(零 LLM):gd_builder.build_game_definition(snapshot, ctx, caps) 纯函数构建;ctx 携 blueprint_id/blueprint_version_no/compiler_version/ruleset_version/rules_hash |
构建异常 → status=failed + error_msg,绝不产出半成品 GD |
| ⑤ | 算指纹:canonical_json + strip_volatile + sha256 → content_fingerprint(64 位) |
序列化异常(如 NaN)→ status=failed |
| ⑥ | 落库(append-only + 幂等命中):先 SELECT ... WHERE tenant_id AND content_fingerprint LIMIT 1;命中 → 复用既有 game_def_id,返回 idempotent=true;未命中 → INSERT pbl_game_definition(uk_gd_fp 唯一约束兜并发) |
唯一冲突 → 回读既有行复用(不新建 id、不产生重复行) |
| ⑦ | 回填任务:status=success + game_def_id + content_fingerprint + duration_ms + finished_at |
回填失败 → 任务留 running 并落审计告警(GD 已落库不回滚,可重跑幂等命中) |
| ⑧ | 写审计:write_audit() 记录编译动作(操作人 / 蓝图 / 版本 / 指纹 / 结果) |
审计失败不阻断主流程,但记 error 日志 |
返回:{task_no, status, game_def_id, fingerprint, idempotent, ...}。
失败路径统一原则(fail-closed):任一步失败都必须 ① 把 error_code(PBL_E_*)/error_msg 写回
pbl_compile_task、② 落审计、③ 不产生 GD 行、④ 返回结构化错误而非 500 裸异常。
7.2 GD 固定 10 个顶层键(gd_builder.GD_TOP_KEYS,缺任一即编译失败 PBL_E_COMPILE)
manifest, pbl, world, scenes, entities, events, states, capabilities, assessment, assets
| 键 | 内容 | 构建函数 |
|---|---|---|
manifest |
编译元数据:schema=pbl.game_definition.v1、gdTopKeys、blueprintId/blueprintVersion、compilerVersion、rulesetVersion、rulesHash、registryHash、fingerprint、deterministic=True、llmUsed=False、counts;createdAt/durationMs 写入正文供审计但被 VOLATILE_KEYS 排除在指纹外 |
build_manifest |
pbl |
PBL 驱动问题 / 学习目标 / 阶段(dimension 缺省 knowledge) |
build_pbl |
world |
世界定义(关联 world_id) |
build_world |
scenes |
场景序列(语义有序,保持原序) | build_scenes |
entities |
实体集合(按 entity_key 排序) |
build_entities |
events |
事件 + response[].capability(能力引用来源之一) |
build_events |
states |
状态机(键排序输出) | build_states |
capabilities |
{schema: pbl.capability_list.v1, registry_hash, items[]};items 为注册表行 ∪ 蓝图引用但未注册的能力(后者标 registered=false,供 M4b fail-closed 裁决告警) |
build_capabilities |
assessment |
pbl_rubric 只读权重结构(34 章,M6 消费) |
build_assessment |
assets |
资产清单(kind+asset_key 排序,url_hash/mime/runtime_neutral=True) |
build_assets |
manifest.fingerprint由fingerprint_of()对 GD 正文(strip_volatile后)取 sha256 得到并回填,manifest内易变项已被VOLATILE_KEYS排除,避免自指循环。
8. 陷阱(10 条 + 本轮 4 项退回防回归条款)
| # | 陷阱 | 正确做法 |
|---|---|---|
| P1 | registry_hash 口径不一致:一侧对原始注册表行取哈希,另一侧对补齐派生字段(args_schema_hash/registered/used_by_gd/category)后的 capabilities.items 取哈希 → 永不相等,自检 G4 红、安装门禁自证失败 |
统一以 GD 内 capabilities.items 为唯一口径;canonical.registry_hash() 同时兼容原始行与派生行(派生值即 short_hash(args_schema_json,16),两路同值);改口径必须同步改自检并重跑到 exit=0 |
| P2 | README/SKILL 端点表与磁盘不符(列出磁盘不存在的 .dspy,或漏列磁盘已有的) |
端点表以 ls wwwroot/api/*.dspy 实测生成后逐行核对;交付前跑 §3.2 ②③④⑤,输出必须为空 |
| P3 | 声称 write_file 的文件实际不存在(幽灵文件)——按造假直接退回 | 交付清单只列磁盘真实文件;交付前 wc -c + cat 复核字节数与正文一致 |
| P4 | 硬编码 DB 名(DB = 'pbl')→ 换库 / 多租户部署即断 |
一律 get_module_dbname('pbl_compiler')(.py 内经 ServerEnv(),.dspy 内直接调全局);跑 §3.2 ⑦ |
| P5 | 格式串占位符与参数个数不匹配 → register() TypeError,RBAC 收口失败 |
%d/%s 个数 == 参数个数;改后 python3 -c "...register()" 实跑通过再交付 |
| P6 | 三处同步漏一处 → .dspy 路由不可达 / NameError: name 'xxx' is not defined |
新增契约同时改 api.py(+__all__) / __init__.py / init.py / load_path.py;grep -rn '<func>' pbl_compiler/ --include='*.py' 必须命中三处 |
| P7 | volatile 字段混入指纹(created_at/duration_ms/task_no/id)→ 每次编译指纹都不同,uk_gd_fp 幂等彻底失效 |
落库前必过 strip_volatile;G3/G5 断言守护;新增易变字段必须同步加进 VOLATILE_KEYS |
| P8 | 文档描述磁盘不存在的代码文件(如声称有 compiler.py/versioning.py/模块级 build.sh) |
目录树以 find . -type f 实测为准;先落文件再写文档,或按实际实现位置描述 |
| P9 | 指纹路径引入不确定性(datetime.now()/uuid/random/无 ORDER BY 的 SQL/依赖 dict 迭代序/NaN) |
指纹路径纯函数化;id 用 stable_id() 派生;SQL 必带确定性 ORDER BY;无序集合先按规范串排序;allow_nan=False |
| P10 | return 写在 async with db.sqlorContext(...) 内 → 静默返回 None(return data type error, <class 'NoneType'>);另 sqlor 只有 C/U/D/R/I/sqlExe,sor.I 只接 1 个参数 |
在 async with 内收集结果,退出块之后再 return;查询走 pbl_common.api 的 q_all/q_one(已适配) |
本轮(qc_reject)4 项退回防回归条款
- 自检脚本必须真实落盘且实跑通过:
scripts/test_m3a_selfcheck.py磁盘存在(19121 B),覆盖 G1-G6; 任何改动后必须重跑并把实测输出(含--- summary ---与exit=)附进交付摘要。 声称写入而磁盘不存在 = 造假,直接退回。 本轮实测:pass=37 fail=0,exit=0。 - 8 个契约必须导出 + 注册:
pbl_compiler_task_get/pbl_compiler_task_list/pbl_game_definition_get/pbl_game_definition_get_by_blueprint/pbl_compiler_verify_determinism/pbl_compiler_version_register/pbl_compiler_version_get/pbl_compiler_version_diff已在api.py:__all__+__init__.py导出 +init.py注册 env +load_path.py补 RBAC 路径(§3.2 ②③④⑤ 实测无输出)。verify_determinism是 US-11 验收唯一入口,漏接线即核心需求缺失。 register()不得崩溃:格式串占位符与参数严格对齐(P5);离线环境返回False+PENDING明细属预期 fail-safe, 判据是「不抛异常」(§3.2 ⑥ 实测通过)。- 禁硬编码 DB 名:见 P4;§3.2 ⑦ 实测输出
OK no hardcoded db。
9. 与 skill/SKILL.md 的双向同步
skill/SKILL.md 是面向 agent 的模块技能文档(YAML frontmatter name/description + 正文:定位、挂载、
数据表、契约端点、陷阱、依赖),本 README 是面向人的完整模块文档。两者关系与同步规则:
- 事实源:README 是事实源(4 张表 / 15 个端点 / 目录树 / ①-⑧ 流程 / 10 条陷阱); SKILL.md 是其浓缩版,供 agent 加载后快速上手,不得与 README 冲突。
- 必须双向同步的 5 类内容:
① 4 张表名与关键约束(尤其
UNIQUE(tenant_id, content_fingerprint)=uk_gd_fp); ② 15 个契约端点清单(数量与名称); ③canonical.py确定性规则(sort_keys/ 零空白 /ensure_ascii=False/ 浮点定点 /strip_volatile/ sha256 /stable_id); ④ GD 10 顶层键(manifest, pbl, world, scenes, entities, events, states, capabilities, assessment, assets); ⑤ §8 陷阱清单(SKILL.md 至少保留 P1-P7 + 本轮 4 项防回归条款)。 - 变更流程:改表 / 改端点 / 改指纹规则时,同一次提交内同时改
README.md与skill/SKILL.md; 只改一处 = 文档失真(历史退回同类问题:SKILL.md 曾写「3 张表 / 7 个契约」与磁盘 4 表 / 15 端点不符,本轮已对齐)。 - 自查命令(应无
SYNC_MISSING输出):cd modules/pbl_compiler for k in pbl_game_definition pbl_compiler_version pbl_capability_registry pbl_compile_task \ content_fingerprint registry_hash strip_volatile verify_determinism \ manifest capabilities assessment assets; do grep -q "$k" README.md && grep -q "$k" skill/SKILL.md || echo "SYNC_MISSING $k"; done
10. 上下游依赖
10.1 上游(本模块依赖)
| 依赖 | 类型 | 用途 | 缺失时行为(fail-closed) |
|---|---|---|---|
pbl_common |
项目模块 | 租户上下文 tenant_id()、DB 适配 q_all/q_one、错误码 PBL_E_*、write_audit、CRUD 工厂 |
取不到租户/库名 → 直接抛错,不建任务不落库 |
pbl_blueprint |
项目模块 | 蓝图聚合根 + 7 类子对象 + 版本快照(编译输入源)、审批记录(is_approved) |
蓝图/版本不存在 → PBL_E_NOT_FOUND;未审批 → PBL_E_STATE_ILLEGAL(403) |
pbl_validation |
项目模块 | 14 维校验产出的 quality_state(5 级阶梯)作为编译质量门禁输入 |
低于 COMPILE_GATE_MIN_STATE → 拒绝编译 |
pbl_appcodes |
项目模块 | 枚举编码(pbl_compile_status / quality_state / category 等) |
编码缺失 → 下拉/校验降级,不阻断编译 |
world / scene |
项目模块 | GD 落库目标(world_id / scene_id),供 M9 渲染 |
无法落库 → 编译失败并回写 error_code |
ahserver / ServerEnv |
基础模块(只读) | load_pbl_compiler() 挂载、get_module_dbname、.dspy 运行时全局 |
模块无法运行(宿主必须先 ServerEnv()) |
sqlor |
基础模块(只读) | sor.C/R/U/D/I/sqlExe + sqlorContext / get_sor_context |
无数据访问能力 |
appbase / rbac |
基础模块(只读) | 编码表、权限路径注册(scripts/load_path.py → set_role_perm.py) |
端点 403 |
10.2 下游(依赖本模块)
| 消费方 | 消费的契约 / 数据 |
|---|---|
pbl_agent_runtime(M4a/M4b) |
pbl_game_definition_get* 读 GD 作 Designer/Critic Agent 上下文;capabilities.items[].registered/permission_required 作 fail-closed 工具裁决依据 |
pbl_evidence(M5a/M5b) |
GD assessment/events/objectives 作为证据采集与幂等比对锚点;引用 content_fingerprint 保证可追溯 |
pbl_assessment(M6) |
GD assessment(Rubric 只读权重结构)+ 指纹追溯 |
pbl_kdb_ext(M7) |
只读引用 GD 指纹做匿名聚合分组(零写入) |
pbl_scense_ext / scense(M9) |
pbl_game_definition_get_by_blueprint 取 GD 驱动前端游戏页渲染(world_id/scene_id) |
pbl_runtime_ext(M11a/M11b) |
GD entities/events/states 作为运行时事件与状态写入的定义源 |
10.3 开发顺序(拓扑)
pbl_common → pbl_appcodes → pbl_blueprint(M1a/M1b) → pbl_validation(M2) →
pbl_compiler(M3a/M3b) → pbl_agent_runtime(M4a/M4b) → pbl_evidence(M5a/M5b) →
pbl_assessment(M6) → pbl_kdb_ext(M7) → pbl_scense_ext(M9) → pbl_runtime_ext(M11a/M11b)。
附:交付前复核清单(每项都必须有磁盘实测证据,禁止口头声明)
wc -c README.md与本文档正文字节数一致;cat抽查非占位、无「见磁盘」类自我指涉语ls wwwroot/api/*.dspy | wc -l== 15,且与 §3 表格逐行一致- §3.2 ②③④⑤ 输出为空;⑥
register()无异常;⑦ 输出OK no hardcoded db;⑧COMPILE_OK python3 scripts/test_m3a_selfcheck.py实测--- summary: pass=37 fail=0 ---、exit=0models/*.json4 份表定义 JSON 合法(json.load通过),四段式 +indexes[].fields为数组README.md与skill/SKILL.md关键词同步自查(§9)无SYNC_MISSING- 交付清单中每个
write_file声称的文件磁盘真实存在(禁幽灵文件)