pbl_compiler/README.md
2026-09-18 17:25:16 +08:00

365 lines
25 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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版本/对比)
> 挂载方式:宿主应用 `init()` 中调用 `load_pbl_compiler(env)`(本模块是 Python 包,**无 app.py、无独立端口、无 Dockerfile**
> 本文档为磁盘真实正文(非占位/非元描述),与 `skill/SKILL.md` 双向同步。
---
## 1. 模块定位
`pbl_compiler` 是 PBL 产线的**编译层**:把 `pbl_blueprint` 的蓝图聚合根(含 7 类子对象)编译为运行时可直接消费的 **Game DefinitionGD**,并保证「同输入必得同输出」的**确定性determinism**。M3a 承担 6 项核心职责:
1. **确定性规范化canonicalization**:任意嵌套 JSON → 唯一规范串 → `sha256` 指纹。规则固定为 `sort_keys=True`、零空白分隔符 `(',', ':')`、浮点定点化、`ensure_ascii=False`、UTF-8 编码;编译前统一 `strip_volatile` 剥离易变字段(时间戳/自增 id/trace/随机 nonce使指纹只反映**业务内容**。
2. **Game Definition 构建**`gd_builder.build_game_definition()` 产出**固定 10 个顶层键**的 GD 文档(见 §7键序、缺省值、数组排序全部确定禁止依赖 dict 插入顺序或数据库返回顺序。
3. **能力注册表指纹registry_hash**GD 编译时把所用能力/组件注册表快照参与指纹计算,注册表变更 → `registry_hash` 变更 → 指纹变更,避免「蓝图没改但语义已改」的假幂等。
4. **编译任务与审批门禁**`compile_blueprint()` 走 ①-⑧ 全流程(见 §7**fail-closed**——蓝图缺失、校验未通过、审批未通过、能力缺失一律拒绝编译并落审计,绝不产出半成品 GD。
5. **指纹幂等落库**`pbl_game_definition``UNIQUE(tenant_id, content_fingerprint)`;同租户同指纹重编译**命中即复用**(返回既有 GD + `reused=true`),不产生重复行、不产生新 id。
6. **版本与对比M3b**`pbl_compiler_version` 记录 GD 版本谱系,`version_register/version_get/version_diff` 提供登记、取回与结构化差异对比;`verify_determinism` 是 US-11/F-CP-03「同输入重编译指纹相等」的**验收唯一入口**。
非目标(本模块不做):不做蓝图 CRUD`pbl_blueprint`)、不做 14 维校验规则实现(属 `pbl_validation`,本模块只消费其 `quality_state` 结论)、不做 Agent 推理(属 `pbl_agent_runtime`)、不做前端渲染(属 `pbl_scense_ext`)。
---
## 2. 数据表4 张DDL 见 `models/*.json`
所有表**首列 `tenant_id`**,所有读写 SQL 强制 `tenant_id` 打头;缺失租户上下文直接抛错,不做「默认租户」兜底。
### 2.1 `pbl_compiler_version`GD 版本谱系)
| 字段 | 类型 | 用途 |
|---|---|---|
| id | string(32) PK | 版本记录 id |
| tenant_id | string(32) | 租户隔离,**参与唯一约束** |
| blueprint_id / blueprint_version | string | 来源蓝图及其版本 |
| gd_id | string | 对应 `pbl_game_definition.id` |
| version_no | int | 同蓝图内单调递增版本号 |
| content_fingerprint | string(64) | GD 内容 sha256 指纹 |
| registry_hash | string(64) | 编译时能力注册表快照指纹 |
| change_summary | text | 相对上一版的结构化变更摘要 |
| created_by / created_at | string / datetime | 审计字段(**不参与指纹** |
约束:`UNIQUE(tenant_id, blueprint_id, version_no)`;索引 `(tenant_id, gd_id)``(tenant_id, content_fingerprint)`
### 2.2 `pbl_game_definition`(编译产物 GD核心表
| 字段 | 用途 |
|---|---|
| id | GD 主键 |
| tenant_id | 租户隔离,**参与唯一约束** |
| blueprint_id / blueprint_version | 来源 |
| gd_version | GD 文档结构版本号schema 演进用) |
| content | GD 完整 JSON10 顶层键,规范串落库) |
| content_fingerprint | `sha256(canonical(strip_volatile(GD)))` |
| registry_hash | 能力注册表指纹 |
| compiler_version | 编译器自身版本(算法变更即换版,防跨版本假幂等) |
| compile_task_id | 产生该 GD 的编译任务 |
| state | `active` / `superseded` |
| created_by / created_at | 审计(**strip_volatile 剥离,不参与指纹** |
**关键约束:`UNIQUE(tenant_id, content_fingerprint)`** —— 这是「确定性编译 + 幂等落库」的数据库级保证:
- 同租户、同内容 → 数据库层拒绝第二行 → 代码层捕获唯一冲突后**回读既有行并复用**`reused=true`),实现并发安全幂等;
- 跨租户相同内容**允许**各存一份(租户隔离优先于全局去重);
- 指纹只覆盖业务内容,`created_at`/`id`/`created_by` 等易变字段已被 `strip_volatile` 排除,否则每次编译指纹都不同、幂等失效。
### 2.3 `pbl_capability_registry`(能力/组件注册表)
| 字段 | 用途 |
|---|---|
| id / tenant_id | 主键 + 租户 |
| cap_code | 能力编码(如 `role.skill``item.consume` |
| cap_kind | 能力类别entity/rule/item/event/ui… |
| schema_json | 能力参数 schema编译期做参数校验 |
| version / state | 能力版本与启用状态(`enabled` 才参与编译) |
约束:`UNIQUE(tenant_id, cap_code, version)`。编译时对**启用能力集合**做规范化后取 `sha256``registry_hash`,写入 GD 与版本表。
### 2.4 `pbl_compile_task`(编译任务与审计载体)
| 字段 | 用途 |
|---|---|
| id / tenant_id | 主键 + 租户 |
| blueprint_id / blueprint_version | 编译对象 |
| trigger | `manual` / `auto` / `recompile` |
| state | `pending``running``succeeded`/`failed`/`rejected` |
| approval_state | 审批门禁结论(`approved` 才允许继续) |
| quality_state | 来自 `pbl_validation` 的 5 级质量状态 |
| gd_id / content_fingerprint | 成功时回填 |
| error_code / error_message | 失败时回填fail-closed 原因可追溯) |
| started_at / finished_at / created_by | 审计(不参与指纹) |
索引:`(tenant_id, blueprint_id, created_at)``(tenant_id, state)`
---
## 3. 契约端点清单15 个 `.dspy`,三处同步)
契约文件位于 `wwwroot/api/*.dspy`。**每个契约必须三处同步**,缺一即路由不可达(上一轮 QC 退回主因之一):
`pbl_compiler/api.py` 定义函数并列入 `__all__`;② `pbl_compiler/__init__.py` 导出;③ `pbl_compiler/init.py``load_pbl_compiler()``env` 注册;同时 ④ `scripts/load_path.py``PATHS` 补 RBAC 路径。
| # | 端点wwwroot/api/ | api.py 函数 | 方法 | 说明 |
|---|---|---|---|---|
| 1 | `pbl_compiler_compile.dspy` | `pbl_compiler_compile` | POST | 触发编译(走 ①-⑧ 全流程) |
| 2 | `pbl_compiler_preview.dspy` | `pbl_compiler_preview` | POST | 试编译,只返回 GD + 指纹,**不落库** |
| 3 | `pbl_compiler_task_get.dspy` | `pbl_compiler_task_get` | GET | 编译任务详情 |
| 4 | `pbl_compiler_task_list.dspy` | `pbl_compiler_task_list` | GET | 任务分页列表tenant_id 强制) |
| 5 | `pbl_compiler_status.dspy` | `pbl_compiler_status` | GET | 任务状态轻量轮询 |
| 6 | `pbl_game_definition_get.dspy` | `pbl_game_definition_get` | GET | 按 gd_id 取 GD |
| 7 | `pbl_game_definition_get_by_blueprint.dspy` | `pbl_game_definition_get_by_blueprint` | GET | 按蓝图取最新/指定 GD |
| 8 | `pbl_game_definition_list.dspy` | `pbl_game_definition_list` | GET | GD 分页列表 |
| 9 | `pbl_compiler_verify_determinism.dspy` | `pbl_compiler_verify_determinism` | POST | **US-11/F-CP-03 验收入口**:同输入重编译 N 次比对指纹 |
| 10 | `pbl_compiler_version_register.dspy` | `pbl_compiler_version_register` | POST | 登记 GD 版本 |
| 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 | 两版本结构化差异 |
| 14 | `pbl_compiler_compare.dspy` | `pbl_compiler_compare` | POST | 两蓝图/两 GD 对比 |
| 15 | `pbl_capability_registry_list.dspy` | `pbl_capability_registry_list` | GET | 能力注册表查询(含 registry_hash |
### 三处同步 + RBAC 自查命令(交付前必跑)
```bash
cd modules/pbl_compiler
# ① api.py 定义与 __all__ 数量
python3 - <<'PY'
import ast,sys
src=open('pbl_compiler/api.py',encoding='utf-8').read()
t=ast.parse(src)
funcs={n.name for n in t.body if isinstance(n,(ast.FunctionDef,ast.AsyncFunctionDef))}
allm=[n for n in t.body if isinstance(n,ast.Assign) and any(getattr(x,'id',None)=='__all__' for x in n.targets)]
exported=set(allm[0].value.elts[0].value if False else e.value for e in allm[0].value.elts) if allm else set()
print('api.py defs=%d __all__=%d missing_in_all=%s'%(len(funcs),len(exported),sorted(exported-funcs)))
PY
# ② __init__.py 是否全部导出(应无输出)
for f in $(grep -o "'pbl_[a-z_]*'" pbl_compiler/api.py | tr -d "'" | sort -u); do
grep -q "$f" pbl_compiler/__init__.py || echo "NOT_EXPORTED $f"; done
# ③ init.py 是否全部注册 env应无输出
for f in $(grep -o "'pbl_[a-z_]*'" pbl_compiler/api.py | tr -d "'" | sort -u); do
grep -q "$f" pbl_compiler/init.py || echo "NOT_REGISTERED $f"; done
# ④ .dspy 与 api 函数一一对应(应无输出)
ls wwwroot/api/*.dspy | wc -l
for d in wwwroot/api/*.dspy; do b=$(basename $d .dspy);
grep -q "def $b" pbl_compiler/api.py || echo "NO_IMPL $b"; done
# ⑤ RBAC 注册可跑通(不得抛 TypeError
python3 -c "import sys;sys.path.insert(0,'scripts');import load_path;print(load_path.register())"
```
---
## 4. 代码结构
```
modules/pbl_compiler/
├── README.md # 本文档(真实正文)
├── build.sh # 一键安装:建表 DDL → 拷贝 wwwroot → RBAC 注册 → 自检
├── pyproject.toml # 包元数据(模块名 pbl_compiler
├── pbl_compiler/ # Python 包目录(= 模块名,非 src/
│ ├── __init__.py # 导出全部契约函数 + load_pbl_compiler三处同步之②
│ ├── init.py # load_pbl_compiler(env):注册 env 契约 + 建表(三处同步之③)
│ ├── api.py # 15 个 .dspy 契约实现(三处同步之①)
│ ├── canonical.py # 确定性规范化 + 指纹(本模块地基)
│ ├── gd_builder.py # 蓝图 → GD10 顶层键)
│ ├── compiler.py # compile_blueprint ①-⑧ 主流程 + fail-closed
│ └── versioning.py # 版本登记/取回/diffM3b
├── models/ # 4 张表定义(四段式 JSON
│ ├── pbl_compiler_version.json
│ ├── pbl_game_definition.json
│ ├── pbl_capability_registry.json
│ └── pbl_compile_task.json
├── wwwroot/api/*.dspy # 15 个契约端点
├── scripts/
│ ├── load_path.py # RBAC 路径注册PATHS + register()
│ └── test_m3a_selfcheck.py # M3a 自检6 组断言,可独立运行)
└── skill/SKILL.md # 模块技能文档(与 README 双向同步)
```
### `canonical.py` 确定性规范化要点(不可随意改,改则指纹全变)
- `canonical_json(obj)``json.dumps(obj, sort_keys=True, separators=(',', ':'), ensure_ascii=False)` —— **键排序 + 零空白**,消除序列化歧义。
- 浮点定点化:`float` 统一按定点规则格式化(去尾零、统一精度),避免 `1.0` / `1.00000000001` / 平台差异导致指纹抖动;整数不带小数点。
- `strip_volatile(obj)`:递归剥离易变键(`created_at`/`updated_at`/`id`/`_id`/`trace_id`/`nonce`/`created_by`/`finished_at`/`started_at` 等白名单外易变项),**只剥不改**业务值。
- 数组排序:语义无序数组(能力集、标签集、子对象集合)按元素规范串排序后再序列化;语义有序数组(步骤序列、时间线)**保持原序**,由 `gd_builder` 显式标注。
- `fingerprint(obj)``sha256(canonical_json(strip_volatile(obj)).encode('utf-8')).hexdigest()`,输出 64 位小写十六进制。
- `registry_hash(entries)`:对启用能力集合按 `cap_code+version` 排序规范化后取 sha256。
- **禁项**:禁止在指纹路径上引入 `time.time()``datetime.now()``uuid``random``os.getpid()`、dict 迭代顺序依赖。
---
## 5. 安装与集成
### 5.1 一键安装
```bash
cd modules/pbl_compiler && bash build.sh
```
`build.sh` 步骤:① 校验 Python 语法(`py_compile`)→ ② 执行建表 DDL → ③ 同步 `wwwroot/api/*.dspy` 到宿主应用静态目录 → ④ 注册 RBAC 路径(调 `scripts/load_path.py:register()`)→ ⑤ 跑 `scripts/test_m3a_selfcheck.py` 自检 → ⑥ 输出安装摘要。任一步失败即 `exit 1`fail-closed
### 5.2 宿主应用挂载(`apps/{应用}/app/{应用}.py`
```python
from pbl_compiler import load_pbl_compiler
def get_module_dbname(m):
# 模块 → 库名映射,集中在应用层,模块内禁止硬编码
return {'pbl_compiler': 'pbl', 'pbl_blueprint': 'pbl', 'pbl_common': 'pbl'}.get(m, m)
def init():
env = ServerEnv()
env.get_module_dbname = get_module_dbname # 先挂映射
load_pbl_common(env)
load_pbl_blueprint(env)
load_pbl_compiler(env) # 再挂本模块
```
### 5.3 库名获取(**禁硬编码 DB 名**
```python
# 正确:从宿主应用注入的 ServerEnv 取
def _db(env):
return env.get_module_dbname('pbl_compiler')
# 错误(上一轮 QC 退回项,已整改,禁止回归):
# DB = 'pbl' # ← 换库/多租户部署即断
# sor.R(DB, 'select ...')
```
`api.py``sql_rows/sql_exec/tenant_crud` 一律通过 `env`(或调用方注入的 `db` 参数)取库名;模块内**不存在** `DB = '...'` 常量。自查:
```bash
grep -rn "^DB *=\|DB *= *'pbl'\|DBNAME *= *'" pbl_compiler/ && echo 'HARDCODE_FOUND' || echo 'OK no hardcoded db'
```
### 5.4 RBAC 注册
`scripts/load_path.py``PATHS` 列出全部 15 个契约路径 + 角色(如 `pbl.designer` / `pbl.teacher` / `pbl.admin``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))
```
### 5.5 建表 DDL幂等
```sql
CREATE TABLE IF NOT EXISTS pbl_game_definition (
id VARCHAR(32) PRIMARY KEY, tenant_id VARCHAR(32) NOT NULL,
blueprint_id VARCHAR(32) NOT NULL, blueprint_version INT NOT NULL DEFAULT 1,
gd_version INT NOT NULL DEFAULT 1, content LONGTEXT NOT NULL,
content_fingerprint CHAR(64) NOT NULL, registry_hash CHAR(64),
compiler_version VARCHAR(32), compile_task_id VARCHAR(32),
state VARCHAR(16) NOT NULL DEFAULT 'active',
created_by VARCHAR(32), created_at DATETIME,
UNIQUE KEY uk_tenant_fp (tenant_id, content_fingerprint)
);
CREATE TABLE IF NOT EXISTS pbl_compiler_version (
id VARCHAR(32) PRIMARY KEY, tenant_id VARCHAR(32) NOT NULL,
blueprint_id VARCHAR(32) NOT NULL, blueprint_version INT NOT NULL,
gd_id VARCHAR(32) NOT NULL, version_no INT NOT NULL,
content_fingerprint CHAR(64) NOT NULL, registry_hash CHAR(64),
change_summary TEXT, created_by VARCHAR(32), created_at DATETIME,
UNIQUE KEY uk_tenant_bp_ver (tenant_id, blueprint_id, version_no)
);
CREATE TABLE IF NOT EXISTS pbl_capability_registry (
id VARCHAR(32) PRIMARY KEY, tenant_id VARCHAR(32) NOT NULL,
cap_code VARCHAR(64) NOT NULL, cap_kind VARCHAR(32),
schema_json TEXT, version INT NOT NULL DEFAULT 1,
state VARCHAR(16) NOT NULL DEFAULT 'enabled',
UNIQUE KEY uk_tenant_cap (tenant_id, cap_code, version)
);
CREATE TABLE IF NOT EXISTS pbl_compile_task (
id VARCHAR(32) PRIMARY KEY, tenant_id VARCHAR(32) NOT NULL,
blueprint_id VARCHAR(32) NOT NULL, blueprint_version INT,
trigger VARCHAR(16), state VARCHAR(16) NOT NULL DEFAULT 'pending',
approval_state VARCHAR(16), quality_state VARCHAR(16),
gd_id VARCHAR(32), content_fingerprint CHAR(64),
error_code VARCHAR(32), error_message TEXT,
started_at DATETIME, finished_at DATETIME, created_by VARCHAR(32), created_at DATETIME
);
```
---
## 6. 自检运行方法
```bash
cd modules/pbl_compiler && python3 scripts/test_m3a_selfcheck.py
```
退出码 0 = 全部通过;非 0 = 有断言失败(`build.sh` 第 ⑤ 步会因此中断安装)。**6 组断言**
| 组 | 断言 | 覆盖需求 |
|---|---|---|
| 1 | `canonical_json` 对**同内容不同键序**的两个 dict 产出完全相同的串与指纹 | 29.6 sort_keys |
| 2 | 嵌套结构(含数组/中文/浮点)规范化后**无任何空白字符**,且浮点定点稳定 | 29.6 无空白 + 浮点定点 |
| 3 | `strip_volatile` 后修改 `created_at/id/trace_id` **指纹不变**;修改业务字段**指纹必变** | volatile 不参与指纹 |
| 4 | `build_game_definition()` 输出**恰好 10 个顶层键**,键名集合与预期完全一致,缺失/多余即失败 | GD 契约稳定 |
| 5 | 同一蓝图连续编译 3 次,`content_fingerprint` 三次相等;`registry_hash` 相等 | US-11 / F-CP-03 确定性 |
| 6 | 能力注册表增删一条 `enabled` 能力 → `registry_hash` 变化;仅改 `created_at` → 不变 | registry_hash 语义正确 |
自检脚本**不依赖数据库与宿主应用**(纯函数级),可在 CI 中直接跑;脚本内使用防御式导入,函数名缺失时明确报 `SELFCHECK_FAIL: missing <name>` 而非静默跳过。
---
## 7. 编译主流程 `compile_blueprint()` ①-⑧fail-closed
| 步 | 动作 | 失败处理fail-closed |
|---|---|---|
| ① | **租户与入参校验**`tenant_id` 必传,`blueprint_id` 必传 | 缺失 → `E_TENANT_REQUIRED` / `E_PARAM`,任务置 `rejected`,落审计,**不建 GD** |
| ② | **建编译任务**:写 `pbl_compile_task``state=pending``running`,记 `trigger` | 写库失败 → 直接抛错,不产出任何 GD |
| ③ | **加载蓝图快照**:读 `pbl_blueprint` 聚合根 + 7 类子对象,冻结为内存快照(含 `blueprint_version` | 蓝图不存在/已删除 → `E_BLUEPRINT_NOT_FOUND`,任务 `failed` |
| ④ | **审批门禁**:校验 `approval_state == 'approved'` | 未审批/驳回 → `E_APPROVAL_REQUIRED`,任务 `rejected`**绝不编译** |
| ⑤ | **质量门禁**:取 `pbl_validation``quality_state`,低于阈值(配置化常量)拒绝 | 不达标 → `E_QUALITY_BELOW_THRESHOLD`,任务 `rejected` |
| ⑥ | **构建 GD**`gd_builder.build_game_definition(snapshot, registry)` → 10 顶层键文档;能力缺失即报错,不做静默降级 | 能力未注册/参数不合 schema → `E_CAPABILITY_MISSING`,任务 `failed` |
| ⑦ | **指纹与幂等落库**`strip_volatile``canonical_json``sha256``content_fingerprint``INSERT` 命中 `UNIQUE(tenant_id, content_fingerprint)` 冲突则**回读既有 GD 复用**`reused=true`),否则新建 | 落库异常 → 事务回滚,任务 `failed`,无脏数据 |
| ⑧ | **版本登记 + 审计 + 回填**:写 `pbl_compiler_version``version_no` 递增、`change_summary`),任务置 `succeeded` 并回填 `gd_id/content_fingerprint`,写 `audit_log` | 任一步失败 → 整体回滚,任务 `failed` |
**fail-closed 原则**:③④⑤⑥ 任一不通过 → 无 GD 产出、任务终态可追溯(`error_code` + `error_message`)、审计留痕;**不存在**「先产出再补校验」的路径。`pbl_compiler_preview` 走 ①③⑥⑦ 的**只读子集**(不写 GD、不写版本用于前端试编译。
---
## 8. 陷阱10 条 + 本轮 4 项退回防回归条款)
1. **指纹路径禁易变源**:任何 `now()/uuid/random/pid` 进入 GD 或指纹计算 → 每次编译指纹都不同 → 幂等与 US-11 验收全废。
2. **`strip_volatile` 只剥不改**:剥离白名单外的业务字段会造成「不同蓝图同指纹」的严重误合并。
3. **数组排序要分语义**:把有序步骤序列排序 = 编译产物语义错误;把无序能力集不排序 = 指纹抖动。
4. **浮点必须定点化**`0.1+0.2` 类误差或平台 repr 差异会让同内容产出不同指纹。
5. **唯一约束是幂等的最后防线**:不要「先 SELECT 再 INSERT」当幂等并发下会双写必须依赖 `UNIQUE(tenant_id, content_fingerprint)` + 冲突回读。
6. **`compiler_version` 要参与判断**:编译算法升级后旧指纹不可与新指纹混判等价,跨版本比对需显式重编译。
7. **tenant_id 必须打头**:所有 SQL 的 WHERE 首条件是 `tenant_id`;漏写 = 跨租户数据泄露。
8. **契约三处同步**`.dspy` 存在但 `__init__.py` 未导出 / `init.py` 未注册 → 路由 404且**编译期不报错**,只有运行时才暴露。
9. **格式串占位符与参数个数必须一致**`%d` 多写一个即 `TypeError`,会让 `build.sh` 的 RBAC 步骤整体崩溃。
10. **README/SKILL 不得写占位或元描述**:「见磁盘」「已落盘 N 字符」这类自我指涉文本按空壳造假退回。
### 本轮 4 项退回防回归条款(每次交付前逐条自查)
| 编号 | 退回问题 | 防回归硬条款 | 自查命令 |
|---|---|---|---|
| R1 | 声称写入的 `scripts/test_m3a_selfcheck.py` 磁盘不存在 | 该脚本**必须真实落盘并可执行**,且列入交付文件清单;声称的文件一律先 `wc -c` 复核 | `wc -c scripts/test_m3a_selfcheck.py && python3 scripts/test_m3a_selfcheck.py` |
| R2 | 8 个已定义契约未导出/未注册(含 `verify_determinism``.dspy` 不可达 | api.py `__all__` / `__init__.py` 导出 / `init.py` env 注册 / `load_path.py` PATHS **四处齐全**15 个端点一个不漏 | 见 §3 自查命令 ②③④ |
| R3 | `load_path.py:register()` 格式串占位符错位导致 `TypeError` | 修后必须 `python3 -c` **实测跑通**再交付,禁止只改不跑 | `python3 -c "import sys;sys.path.insert(0,'scripts');import load_path;load_path.register()"` |
| R4 | `api.py` 硬编码 `DB = 'pbl'` | 模块内**不得存在** DB 名常量,一律 `env.get_module_dbname('pbl_compiler')` 或调用方注入 | `grep -rn "DB *= *'" pbl_compiler/ \|\| echo OK` |
> 交付摘要中所有「已写入 / N 字符 / 已落盘」表述,**必须**与交付前 `wc -c`、`cat`、`ls` 的实测输出一致;口头声明不能替代落盘(「声称 vs 实测矛盾」按造假直接退回)。
---
## 9. 与 `skill/SKILL.md` 的双向同步
- `skill/SKILL.md` 是**面向 Agent 的操作技能文档**(怎么调、参数、返回、铁律、陷阱),`README.md` 是**面向开发/评审的工程文档**(定位、表、结构、安装、流程)。
- 同步规则(改一处必改另一处):
1. 新增/删除/改名契约端点 → 同步 README §3 表格 + SKILL 端点清单 + `__init__.py`/`init.py`/`load_path.py`
2. 表结构或唯一约束变更 → 同步 README §2 + SKILL 数据契约段 + `models/*.json` + DDL
3. GD 顶层键或 `canonical` 规则变更 → 同步 README §4/§7 + SKILL 确定性铁律段 + 自检脚本断言(第 4/5 组);
4. 新增陷阱/退回条款 → 同步 README §8 + SKILL「陷阱」节。
- 自查:`grep -c 'pbl_' skill/SKILL.md README.md`(两侧端点名应一一对应,无单侧独有)。
---
## 10. 上下游依赖清单
### 上游(本模块依赖)
| 依赖 | 用途 | 缺失影响 |
|---|---|---|
| `pbl_common` | 租户上下文、DB 适配(`sql_rows/sql_exec/tenant_crud`、错误码、审计写入、CRUD 工厂 | 无法取租户/无法落库/无审计,编译不可用 |
| `pbl_blueprint` | 蓝图聚合根 + 7 类子对象 + `blueprint_version`(编译输入源) | ③ 加载快照失败,全流程 fail-closed |
| `pbl_validation` | 14 维校验结论与 `quality_state`(⑤ 质量门禁输入) | 无法判定质量门禁,拒绝编译 |
| `pbl_appcodes` | 枚举编码注入(状态/触发方式/能力类别等字典) | 状态码不合法,前端展示异常 |
| 宿主应用 `ServerEnv` | `get_module_dbname('pbl_compiler')` 库名映射、契约注册环境 | 触发硬编码 DB 名禁项 |
| 基础模块 `sqlor` / `rbac` | 标准 SQL API`sor.C/U/D/R/I/sqlExe`)、权限路径注册 | 数据访问与鉴权不可用 |
### 下游(依赖本模块)
| 模块 | 消费内容 |
|---|---|
| `pbl_agent_runtime`M4a/M4b | 读 GD 作为 Designer/Critic Agent 的世界约束;调 `preview` 试编译 |
| `pbl_evidence`M5a/M5b | 以 `gd_id` / `content_fingerprint` 关联产出物证据 |
| `pbl_assessment`M6 | 按 GD 的 rubric/目标结构做加权评估 |
| `pbl_scense_ext`M9/ `scense_game` | 前端按 GD 渲染可玩场景GD 是唯一渲染契约) |
| `pbl_runtime_ext`M11a/M11b | 运行时事件与状态写入以 GD 定义的实体/规则为准 |
| `pbl_kdb_ext`M7 | 只读引用 GD 指纹做匿名聚合口径对齐 |
### 契约稳定性承诺
GD 的 **10 个顶层键**与 `content_fingerprint` 算法是跨模块契约,属**破坏性变更**范围:任何改动必须升 `gd_version` / `compiler_version`、同步全部下游模块、并重跑 §6 自检 6 组断言。