# 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 项核心职责: 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.1),**fail-closed**:未审批、质量状态未达门禁、蓝图/版本缺失一律拒绝编译, 错误码 `PBL_E_*` 回写任务并落审计,绝不产出半成品 GD。 5. **指纹幂等落库** `pbl_game_definition` 上 `UNIQUE 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_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. = ` 注册; ④ `scripts/load_path.py` 的 `PATHS` 补该端点 RBAC 路径(**禁止通配符 `%` / `*`**,逐条显式;当前 15 条,角色 `logined`)。 ### 3.2 自查命令(交付前必跑;②③④⑤ 输出必须为空,⑥ 不得抛异常,⑦ 必须输出 OK) ```bash 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) ```bash # ① 安装模块包(模块仓库根目录) cd modules/pbl_compiler && pip install . # ② 建表(幂等 DDL;库名由宿主应用决定,勿写死) mysql -h -u -p < sql/pbl_compiler.sql # ③ 前端链接:模块 wwwroot 软链到宿主应用 wwwroot(软链非 cp,保持同步) ln -sf /modules/pbl_compiler/wwwroot /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`) ```python 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) ```python # ✅ 正确(.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` 已修,禁止回归): ```python 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 ` 并返回 `False`(部署到真实环境后返回 `True`)。这是预期的 fail-safe 行为, 关键判据是「**不得抛 TypeError**」(§3.2 ⑥ 实测通过)。 ### 5.5 建表 DDL(幂等,完整见 `sql/pbl_compiler.sql`) ```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`) ```bash 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 ` 并计入 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 '' 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, `);另 sqlor 只有 `C/U/D/R/I/sqlExe`,`sor.I` 只接 1 个参数 | 在 `async with` 内收集结果,**退出块之后**再 `return`;查询走 `pbl_common.api` 的 `q_all`/`q_one`(已适配) | ### 本轮(qc_reject)4 项退回防回归条款 1. **自检脚本必须真实落盘且实跑通过**:`scripts/test_m3a_selfcheck.py` 磁盘存在(19121 B),覆盖 G1-G6; 任何改动后必须重跑并把实测输出(含 `--- summary ---` 与 `exit=`)附进交付摘要。 **声称写入而磁盘不存在 = 造假,直接退回。** 本轮实测:`pass=37 fail=0`,`exit=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.md` 与 `skill/SKILL.md`; 只改一处 = 文档失真(历史退回同类问题:SKILL.md 曾写「3 张表 / 7 个契约」与磁盘 4 表 / 15 端点不符,本轮已对齐)。 - **自查命令**(应无 `SYNC_MISSING` 输出): ```bash 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)。 --- ## 附:交付前复核清单(每项都必须有磁盘实测证据,禁止口头声明) - [x] `wc -c README.md` 与本文档正文字节数一致;`cat` 抽查非占位、无「见磁盘」类自我指涉语 - [x] `ls wwwroot/api/*.dspy | wc -l` == 15,且与 §3 表格逐行一致 - [x] §3.2 ②③④⑤ 输出为空;⑥ `register()` 无异常;⑦ 输出 `OK no hardcoded db`;⑧ `COMPILE_OK` - [x] `python3 scripts/test_m3a_selfcheck.py` 实测 `--- summary: pass=37 fail=0 ---`、`exit=0` - [x] `models/*.json` 4 份表定义 JSON 合法(`json.load` 通过),四段式 + `indexes[].fields` 为数组 - [x] `README.md` 与 `skill/SKILL.md` 关键词同步自查(§9)无 `SYNC_MISSING` - [x] 交付清单中每个 `write_file` 声称的文件磁盘真实存在(禁幽灵文件)