pbl_compiler/README.md
2026-09-18 18:24:31 +08:00

559 lines
42 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 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.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 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`/`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
```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 # 幂等建表 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` / 平台差异导致指纹抖动;
整数不带小数点(`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 <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`
```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 <role> <path>` 并返回 `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 <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_reject4 项退回防回归条款
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` 声称的文件磁盘真实存在(禁幽灵文件)