--- name: pbl_compiler description: PBL Compiler v1(确定性编译 + Game Definition,M3a/M3b)——蓝图版本快照 → GD 10 顶层键,29.6 确定性(sort_keys/零空白/strip_volatile/sha256/registry_hash),UNIQUE(tenant_id, content_fingerprint) 幂等落库,审批+质量双门禁 fail-closed。改本模块前先读「铁律」与「陷阱」两节。 --- # pbl_compiler 模块技能 > 本文档是 `README.md` 的**浓缩版**(面向 agent)。README 为事实源,两者必须双向同步(同步规则见 README §9)。 > 磁盘实测:15 个 `.dspy` 契约、4 张表、`api.py:__all__` 35 项、自检 `pass=37 fail=0`(`exit=0`)。 ## 定位 PBL 产线**编译层**:把 `pbl_blueprint` 的蓝图版本快照(含 7 类子对象)编译为运行时可直接消费的 **Game Definition(GD)**,保证「同输入必得同输出」(需求 29.6 / US-11 / F-CP-03)。 **纯确定性、零 LLM**(`manifest.llmUsed=False`、`deterministic=True`)。 不做:蓝图 CRUD(`pbl_blueprint`)、14 维校验实现(`pbl_validation`,只消费其 `quality_state`)、 Agent 推理(`pbl_agent_runtime`)、前端渲染(`pbl_scense_ext`/`scense`)、证据采集(`pbl_evidence`)。 ## 挂载 ```python from pbl_compiler.init import load_pbl_compiler # 应用 apps/pbls/app/pbls.py 的 init() 中按序调用 load_pbl_compiler() # 注册 15 契约 + 13 内部函数到 ServerEnv ``` 本模块是 **Python 包(业务模块)**:**无 `app.py`、无端口、无 Dockerfile、无模块级 `build.sh`**—— 安装步骤由宿主应用 `apps/pbls/build.sh` 承担(pip install → `sql/pbl_compiler.sql` 建表 → wwwroot 软链 → `scripts/load_path.py` RBAC → `py_compile` + `scripts/test_m3a_selfcheck.py` 门禁)。 ## 数据表(4 张,`models/*.json` 四段式 + `sql/pbl_compiler.sql`) 所有表**首列 `tenant_id`**,所有 SQL 强制带 `tenant_id`;缺失租户上下文 fail-closed 抛错,无「默认租户」兜底。 | 表 | 用途 | 关键约束 | |---|---|---| | `pbl_game_definition` | GD 编译产物(append-only):`content_fingerprint`/`definition_json`/`compiler_version_id`/`world_id`/`scene_id`/`entity_count`/`event_count`/`quality_state`/`duration_ms` | **`UNIQUE uk_gd_fp (tenant_id, content_fingerprint)`** ← 确定性幂等的 DB 级保证;`idx_gd_bp (tenant_id, blueprint_id)` | | `pbl_compiler_version` | 编译器版本谱系:`code`/`semver`/`rules_hash`/`entrypoint`/`enabled`/`notes` | `UNIQUE uk_cv_code (tenant_id, code)` | | `pbl_capability_registry` | 能力注册表:`capability_key`/`category`/`args_schema_json`/`permission_required`/`is_enabled`/`version_no` | `UNIQUE uk_cap_key (tenant_id, capability_key, version_no)` | | `pbl_compile_task` | 编译任务与审计载体:`task_no`(确定性派生)/`status`/`approval_id`/`game_def_id`/`content_fingerprint`/`error_code`/`error_msg`/`triggered_by`/`duration_ms` | `UNIQUE uk_ct_task_no (tenant_id, task_no)`;`idx_ct_blueprint`、`idx_ct_status` | CRUD 定义(`json/`):`compiler_pbl_game_definition.json`、`compiler_pbl_compiler_version.json`、 `compiler_pbl_capability_registry.json`(`pbl_compile_task` 为纯后端任务表,无 CRUD 页面)。 ## 契约接口(15 个,路径 `/pbl_compiler/api/.dspy`) `pbl_compiler_compile`(POST 编译主流程) · `pbl_compiler_preview`(POST 试编译不落库) · `pbl_compiler_compare`(POST 对比) · **`pbl_compiler_verify_determinism`(POST,US-11/F-CP-03 验收唯一入口)** · `pbl_compiler_task_get` · `pbl_compiler_task_list` · `pbl_game_definition_get` · `pbl_game_definition_get_by_blueprint` · `pbl_compiler_version_register` · `pbl_compiler_version_save` · `pbl_compiler_version_get` · `pbl_compiler_version_list` · `pbl_compiler_version_diff` · `pbl_capability_list` · `pbl_capability_register` 内部函数(非 `.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`。 ## 代码结构(磁盘实测) ``` pbl_compiler/{__init__.py, init.py, api.py, canonical.py, gd_builder.py} models/{pbl_game_definition,pbl_compiler_version,pbl_capability_registry,pbl_compile_task}.json json/compiler_pbl_*.json sql/pbl_compiler.sql wwwroot/{index.ui, api/*.dspy ×15} scripts/{load_path.py, test_m3a_selfcheck.py} skill/SKILL.md pyproject.toml README.md ``` **磁盘无** `compiler.py` / `versioning.py` / 模块级 `build.sh` / `app.py`——编译主流程在 `api.py:compile_blueprint()`, GD 构建在 `gd_builder.py`。文档禁止描述磁盘不存在的文件。 ## 确定性规则(`canonical.py`,改则历史指纹全变) - `canonical_json`:`json.dumps(sort_keys=True, separators=(',',':'), ensure_ascii=False, allow_nan=False)` —— 键排序 + 零空白 + 中文不转义 + **拒绝 NaN/Infinity**。 - 浮点定点化(固定精度、去尾零);整数不带小数点;`1` 与 `1.0` 稳定且互不混淆。 - `strip_volatile`:递归(含 `list[dict]`)剥离 `id`/`created_at`/`updated_at`/`duration_ms`/`task_no`/ `started_at`/`finished_at`/`created_by`/`trace_id`/`nonce` 等,**只剥不改**业务值。 - 数组稳定化:语义无序集合按元素规范串排序;语义有序(场景步骤、事件时间线)保持原序,由 `gd_builder` 显式决定。 - `fingerprint = sha256(canonical_bytes(strip_volatile(obj)))` → 64 位小写 hex;`short_hash(text, n)` 定长前缀哈希。 - `registry_hash(capabilities)`:归一为 `{capability_key, version_no, args_schema_hash, permission_required, is_enabled}` 五元组 → 排序 → 规范化 → sha256。**入参兼容原始注册表行(`args_schema_json`)与 GD 派生行 (`args_schema_hash`),两路同值**(口径铁律,见陷阱 P1)。 - `stable_id(blueprint_id, version_no, kind, seq)` = `{bp}:{ver}:{kind}:{seq:05d}`——GD 内一切 id 由输入派生,**禁 uuid/random**。 - 禁项:指纹路径出现 `time.time()`/`datetime.now()`/`uuid`/`random`/`os.getpid()`/dict 迭代序依赖/无 `ORDER BY` 的 SQL 行序依赖。 ## 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`) ## 编译主流程 `compile_blueprint()` ①-⑧(fail-closed) ① 审批门禁 `is_approved()`(未审批 → `PBL_E_STATE_ILLEGAL`/403)+ 质量门禁(`quality_state` 未达 `COMPILE_GATE_MIN_STATE` → 拒绝)→ ② 建 `pbl_compile_task(status=running, approval_id, task_no 确定性派生)` → ③ `get_version()` 取蓝图版本快照(输入锁定)→ ④ `build_game_definition(snapshot, ctx, caps)` 纯函数生成 GD 10 键(零 LLM)→ ⑤ `canonical_json`+`strip_volatile`+`sha256` 算 `content_fingerprint` → ⑥ 落库 append-only:先按 `(tenant_id, content_fingerprint)` SELECT,命中即复用(`idempotent=true`),未命中 INSERT(`uk_gd_fp` 兜并发)→ ⑦ 任务 `status=success` + 回填 `game_def_id`/`content_fingerprint`/`duration_ms`/`finished_at` → ⑧ `write_audit`。 返回 `{task_no, status, game_def_id, fingerprint, idempotent, ...}`。 **失败必做**:`error_code`(`PBL_E_*`)/`error_msg` 回写任务 + 落审计 + 不产生 GD 行 + 返回结构化错误(非 500 裸异常)。 ## 自检(`scripts/test_m3a_selfcheck.py`) ```bash cd modules/pbl_compiler && python3 scripts/test_m3a_selfcheck.py ; echo "exit=$?" ``` 退出码 **0 = 6 组全过**,1 = 有 FAIL,2 = 被测模块加载失败。纯函数级(importlib 按路径加载 `canonical.py`/ `gd_builder.py`),**不连库、不依赖 ServerEnv/ahserver**,CI 可直接跑。6 组断言: G1 canonical 键序无关/指纹形态 · G2 零空白/ensure_ascii=False/浮点定点/NaN 拒绝 · G3 strip_volatile 易变字段不参与指纹(含递归 list[dict]、算法口径一致) · G4 GD 10 顶层键 + 指纹一致 + `manifest.registryHash == registry_hash(GD capabilities.items)`(统一口径)+ 派生不丢语义 · G5 同输入重编译指纹全等(3 次)/ctx 易变字段不影响/入库顺序颠倒不影响/业务变更必变 · G6 registry_hash 语义(顺序无关、增删能力/升版/改 schema 必变、易变字段不变、空表确定哈希)。 **本轮实测:`--- summary: pass=37 fail=0 ---`,`exit=0`。** ## 铁律 1. **库名禁硬编码**:一律 `ServerEnv().get_module_dbname('pbl_compiler')`(.py)/ `get_module_dbname('pbl_compiler')`(.dspy 全局)。 模块内**不存在** `DB = '...'` / `DBNAME = '...'`。自查:`grep -rn "^DB *=\|DBNAME *= *'" pbl_compiler/ --include='*.py'` 应无输出。 2. **三处同步 + RBAC**:新增/删除契约必须同改 ① `api.py`(+`__all__`) ② `__init__.py` 导出 ③ `init.py` `env.x = x` ④ `scripts/load_path.py` `PATHS`(**禁通配符 `%`/`*`**,逐条显式)。漏一处 → 路由不可达 / `NameError`。 3. **tenant_id 强制打头**:所有读写 SQL 带 `tenant_id`(`pbl_common.api.tenant_id()`),缺失即 fail-closed。 4. **指纹路径纯函数**:禁时间/随机/uuid/行序依赖;GD 内 id 用 `stable_id()` 派生。 5. **文档与磁盘一致**:README/SKILL 的端点表、目录树、表字段必须与磁盘实测一致;**声称 write_file 的文件必须真实落盘** (幽灵文件按造假直接退回)。 6. **README ↔ SKILL 同一次提交内同步**(表名/约束、15 端点、canonical 规则、GD 10 键、陷阱 P1-P7)。 ## 陷阱 - **P1 registry_hash 口径不一致**(历史两轮退回根因):`gd_builder.build_capabilities()` 用**补齐派生字段后**的 `known.values()` 算 hash,旧自检对**原始注册表行**算 → 永不相等 → G4 FAIL、`exit=1`、安装门禁自证失败。 统一以 **GD 内 `capabilities.items`** 为唯一口径,`canonical.registry_hash()` 兼容两路入参同值。 - **P2 端点表与磁盘不符**:曾列出磁盘不存在的 `pbl_compiler_status.dspy`/`pbl_game_definition_list.dspy`/ `pbl_capability_registry_list.dspy`,漏列磁盘已有的 `pbl_capability_list`/`pbl_capability_register`/ `pbl_compiler_version_save`。以 `ls wwwroot/api/*.dspy` 为准。 - **P3 幽灵文件**:声称写入的 `scripts/test_m3a_selfcheck.py` 曾磁盘不存在 → 按造假退回。 - **P4 硬编码 `DB = 'pbl'`** → 换库/多租户即断。 - **P5 格式串占位符与参数个数不匹配** → `register()` `TypeError: not enough arguments for format string`, RBAC 收口失败(`'[%s] ... total=%d ok=%d pending=%d' % (MODULE, len(PATHS), done, len(missing))`)。 离线环境 `register()` 返回 `False` + 打印 `PENDING ` 属预期 fail-safe,判据是**不抛异常**。 - **P6 三处同步漏一处** → `.dspy` 500 `NameError: name 'xxx' is not defined`。 - **P7 volatile 混入指纹** → 每次编译指纹不同,`uk_gd_fp` 幂等失效;新增易变字段必须同步进 `VOLATILE_KEYS`。 - **P8 文档描述磁盘不存在的代码文件**(`compiler.py`/`versioning.py`/模块 `build.sh`)。 - **P9 指纹路径引入不确定性**(NaN、无 `ORDER BY`、dict 迭代序、`datetime.now()`)。 - **P10 `return` 写在 `async with db.sqlorContext(...)` 内** → 静默 `None`(`return data type error, `); 块内收集、**退出块后**再 `return`。sqlor 只有 `C/U/D/R/I/sqlExe`(`sor.I(ns)` 只 1 个参数); 查询走 `pbl_common.api` 的 `q_all`/`q_one`(已适配)。 - 其它:`.dspy` 内**禁 import**(全局已预载)、禁 `ServerEnv()` 取 per-request 用户(用 `request._run_ns`)、 `py_compile` 对 `.dspy` 无效(用 grep 审计)、`trigger` 是 MySQL 关键字(本模块用 `triggered_by` 规避)、 手工建表必须 `COLLATE utf8mb4_unicode_ci`(否则 `Illegal mix of collations`)。 ## 依赖 **上游**:`pbl_common`(租户/DB 适配/错误码/审计/CRUD 工厂)、`pbl_blueprint`(蓝图 + 版本快照 + 审批)、 `pbl_validation`(`quality_state` 门禁)、`pbl_appcodes`(枚举)、`world`/`scene`(GD 落库目标)、 基础模块 `ahserver`/`sqlor`/`appbase`/`rbac`(只读,禁改)。 **下游**:`pbl_agent_runtime`(M4)、`pbl_evidence`(M5)、`pbl_assessment`(M6)、`pbl_kdb_ext`(M7)、 `pbl_scense_ext`/`scense`(M9)、`pbl_runtime_ext`(M11)。 **开发顺序**:`pbl_common` → `pbl_appcodes` → `pbl_blueprint`(M1) → `pbl_validation`(M2) → **`pbl_compiler`(M3)** → `pbl_agent_runtime`(M4) → `pbl_evidence`(M5) → `pbl_assessment`(M6) → `pbl_kdb_ext`(M7) → `pbl_scense_ext`(M9) → `pbl_runtime_ext`(M11)。