2026-09-18 18:24:31 +08:00

151 lines
13 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.

---
name: pbl_compiler
description: PBL Compiler v1确定性编译 + Game DefinitionM3a/M3b——蓝图版本快照 → GD 10 顶层键29.6 确定性sort_keys/零空白/strip_volatile/sha256/registry_hashUNIQUE(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 DefinitionGD**,保证「同输入必得同输出」(需求 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/<name>.dspy`
`pbl_compiler_compile`(POST 编译主流程) · `pbl_compiler_preview`(POST 试编译不落库) ·
`pbl_compiler_compare`(POST 对比) · **`pbl_compiler_verify_determinism`(POSTUS-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 = 有 FAIL2 = 被测模块加载失败。纯函数级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 <role> <path>` 属预期 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, <class 'NoneType'>`
块内收集、**退出块后**再 `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)。