pbl_compiler —— PBL Compiler v1确定性编译 + Game Definition

模块仓库:modules/pbl_compiler/ | 里程碑:M3a(编译核心与确定性产物)+ M3b版本登记/对比) 挂载方式:宿主应用 apps/pbls/app/pbls.pyinit() 中按序调用 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 DefinitionGD,并保证「同输入必得同输出」的确定性determinism需求 29.6 / US-11。 M3a 承担 6 项核心职责:

  1. 确定性规范化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 等),使指纹只反映业务内容

  2. Game Definition 构建——pbl_compiler/gd_builder.py build_game_definition(snapshot, ctx, registry_rows)纯函数(零 LLM、零 IO、零随机 产出固定 10 个顶层键的 GD 文档(键清单见 §7.2),并返回 64 位 content_fingerprint。 键序、缺省值、数组排序全部显式决定,禁止依赖 dict 插入顺序或数据库返回行序。

  3. 能力注册表指纹registry_hash GD 编译时把所用能力注册表快照纳入 manifest.registryHash 并参与 content_fingerprint 能力集变更(增删 / version_no 升级 / args_schema 变更)→ registry_hash 变更 → 指纹变更, 避免「蓝图没改但语义已改」的假幂等。口径铁律见 §8 P1(历史两轮退回根因位,本轮已统一并加断言守护)。

  4. 编译任务与审批门禁——api.py: compile_blueprint() / 契约 pbl_compiler_compile 走 ①-⑧ 全流程§7.1fail-closed:未审批、质量状态未达门禁、蓝图/版本缺失一律拒绝编译, 错误码 PBL_E_* 回写任务并落审计,绝不产出半成品 GD。

  5. 指纹幂等落库 pbl_game_definitionUNIQUE KEY uk_gd_fp (tenant_id, content_fingerprint) 同租户同指纹重编译命中即复用(返回既有 game_def_id + idempotent=true),不产生重复行、不产生新 id。

  6. 编译器版本与对比M3b pbl_compiler_version 锁定编译器版本(code/semver/rules_hash/entrypoint version_register / version_save / version_get / version_list / version_diff / compare 提供登记、保存、取回、 谱系列表与结构化差异对比;pbl_compiler_verify_determinismUS-11 / F-CP-03「同输入重编译指纹相等」的验收唯一入口

非目标(本模块不做):不做蓝图 CRUDpbl_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-onlyGD 不原地改写,重编译产生新指纹新行,旧行由 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.skillpbl.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 五元组)→ sha256registry_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_statuspending/running/success/failed
approval_id int(20) 关联审批记录(未审批拒绝F-CP-01
game_def_id int(20) 产出的 GD idsuccess 时回填)
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/decindexes[].fields 为数组。CRUD 定义在 json/ compiler_pbl_game_definition.jsoncompiler_pbl_compiler_version.jsoncompiler_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.pyload_pbl_compiler()env.<name> = <name> 注册; ④ scripts/load_path.pyPATHS 补该端点 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            # 幂等建表 DDL4 张表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 / 平台差异导致指纹抖动; 整数不带小数点(11.0 规范化结果稳定且互不混淆)。
  • 拒绝 NaN / Infinityallow_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_hashurl_hashrules_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 / random29.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.pysql_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.uiregister() 逐条注册并返回统计。 格式串占位符个数必须与参数个数一致(历史 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 collationstrigger 是 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.pygd_builder.pyimportlib,绕开包 __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(零 LLMmanifest.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-01is_approved() 校验蓝图版本已审批,取 approval_id + 审批证据;同时校验 quality_stateCOMPILE_GATE_MIN_STATE(第 15 章门禁阶梯 QUALITY_LADDER 未审批 → PBL_E_STATE_ILLEGALHTTP 403质量未达标 → PBL_E_STATE_ILLEGAL(消息含 quality_state=%s 未达 %s)。不建任务、不落 GD
建编译任务:写 pbl_compile_taskstatus=runningapproval_idcompiler_versionruleset_versiontask_no 确定性派生、triggered_bystarted_at 写失败即中止,返回 task_no + error_code
取蓝图版本快照(输入锁定):get_version() 读蓝图聚合根 + 7 类子对象,冻结为 snapshot,固定行序 蓝图/版本不存在 → status=failed + PBL_E_NOT_FOUND
确定性转换生成 GD 10 顶层键(零 LLMgd_builder.build_game_definition(snapshot, ctx, caps) 纯函数构建;ctxblueprint_id/blueprint_version_no/compiler_version/ruleset_version/rules_hash 构建异常 → status=failed + error_msg绝不产出半成品 GD
算指纹canonical_json + strip_volatile + sha256content_fingerprint64 位) 序列化异常(如 NaNstatus=failed
落库append-only + 幂等命中):先 SELECT ... WHERE tenant_id AND content_fingerprint LIMIT 1;命中 → 复用既有 game_def_id,返回 idempotent=true;未命中 → INSERT pbl_game_definitionuk_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_codePBL_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.v1gdTopKeysblueprintId/blueprintVersioncompilerVersionrulesetVersionrulesHashregistryHashfingerprintdeterministic=TruellmUsed=FalsecountscreatedAt/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.fingerprintfingerprint_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() TypeErrorRBAC 收口失败 %d/%s 个数 == 参数个数;改后 python3 -c "...register()" 实跑通过再交付
P6 三处同步漏一处.dspy 路由不可达 / NameError: name 'xxx' is not defined 新增契约同时改 api.py(+__all__) / __init__.py / init.py / load_path.pygrep -rn '<func>' pbl_compiler/ --include='*.py' 必须命中三处
P7 volatile 字段混入指纹created_at/duration_ms/task_no/id)→ 每次编译指纹都不同,uk_gd_fp 幂等彻底失效 落库前必过 strip_volatileG3/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(...) → 静默返回 Nonereturn data type error, <class 'NoneType'>);另 sqlor 只有 C/U/D/R/I/sqlExesor.I 只接 1 个参数 async with 内收集结果,退出块之后return;查询走 pbl_common.apiq_all/q_one(已适配)

本轮qc_reject4 项退回防回归条款

  1. 自检脚本必须真实落盘且实跑通过scripts/test_m3a_selfcheck.py 磁盘存在19121 B覆盖 G1-G6 任何改动后必须重跑并把实测输出(含 --- summary ---exit=)附进交付摘要。 声称写入而磁盘不存在 = 造假,直接退回。 本轮实测:pass=37 fail=0exit=0
  2. 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 验收唯一入口,漏接线即核心需求缺失。
  3. register() 不得崩溃格式串占位符与参数严格对齐P5离线环境返回 False + PENDING 明细属预期 fail-safe 判据是「不抛异常」§3.2 ⑥ 实测通过)。
  4. 禁硬编码 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.mdskill/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_state5 级阶梯)作为编译质量门禁输入 低于 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.pyset_role_perm.py 端点 403

10.2 下游(依赖本模块)

消费方 消费的契约 / 数据
pbl_agent_runtimeM4a/M4b pbl_game_definition_get* 读 GD 作 Designer/Critic Agent 上下文;capabilities.items[].registered/permission_required 作 fail-closed 工具裁决依据
pbl_evidenceM5a/M5b GD assessment/events/objectives 作为证据采集与幂等比对锚点;引用 content_fingerprint 保证可追溯
pbl_assessmentM6 GD assessmentRubric 只读权重结构)+ 指纹追溯
pbl_kdb_extM7 只读引用 GD 指纹做匿名聚合分组(零写入)
pbl_scense_ext / scenseM9 pbl_game_definition_get_by_blueprint 取 GD 驱动前端游戏页渲染(world_id/scene_id
pbl_runtime_extM11a/M11b GD entities/events/states 作为运行时事件与状态写入的定义源

10.3 开发顺序(拓扑)

pbl_commonpbl_appcodespbl_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=0
  • models/*.json 4 份表定义 JSON 合法(json.load 通过),四段式 + indexes[].fields 为数组
  • README.mdskill/SKILL.md 关键词同步自查§9SYNC_MISSING
  • 交付清单中每个 write_file 声称的文件磁盘真实存在(禁幽灵文件)
Description
No description provided
Readme 25 KiB