deliver: 交付收口(引擎代为提交)
This commit is contained in:
parent
1d5f02891a
commit
474fc2b256
714
README.md
714
README.md
@ -1,364 +1,558 @@
|
||||
# pbl_compiler —— PBL Compiler v1(确定性编译 + Game Definition)
|
||||
|
||||
> 模块仓库:`modules/pbl_compiler/` | 里程碑:M3a(编译核心与确定性产物)+ M3b(版本/对比)
|
||||
> 挂载方式:宿主应用 `init()` 中调用 `load_pbl_compiler(env)`(本模块是 Python 包,**无 app.py、无独立端口、无 Dockerfile**)
|
||||
> 本文档为磁盘真实正文(非占位/非元描述),与 `skill/SKILL.md` 双向同步。
|
||||
> 模块仓库:`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)**。M3a 承担 6 项核心职责:
|
||||
`pbl_compiler` 是 PBL 产线的**编译层**:把 `pbl_blueprint` 的蓝图版本快照(含 7 类子对象)编译为运行时可直接消费的
|
||||
**Game Definition(GD)**,并保证「同输入必得同输出」的**确定性(determinism,需求 29.6 / US-11)**。
|
||||
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「同输入重编译指纹相等」的**验收唯一入口**。
|
||||
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` 等),使指纹只反映**业务内容**。
|
||||
|
||||
非目标(本模块不做):不做蓝图 CRUD(属 `pbl_blueprint`)、不做 14 维校验规则实现(属 `pbl_validation`,本模块只消费其 `quality_state` 结论)、不做 Agent 推理(属 `pbl_agent_runtime`)、不做前端渲染(属 `pbl_scense_ext`)。
|
||||
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 张,DDL 见 `models/*.json`)
|
||||
## 2. 数据表(4 张:`models/*.json` 四段式定义 + `sql/pbl_compiler.sql` DDL)
|
||||
|
||||
所有表**首列 `tenant_id`**,所有读写 SQL 强制 `tenant_id` 打头;缺失租户上下文直接抛错,不做「默认租户」兜底。
|
||||
所有表**首列 `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_compiler_version`(GD 版本谱系)
|
||||
### 2.1 `pbl_game_definition`(编译产物 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 | 审计字段(**不参与指纹**) |
|
||||
| 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(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 完整 JSON(10 顶层键,规范串落库) |
|
||||
| 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`),实现并发安全幂等;
|
||||
**关键约束:`UNIQUE KEY uk_gd_fp (tenant_id, content_fingerprint)`** —— 「确定性编译 + 幂等落库」的数据库级保证:
|
||||
- 同租户、同内容 → 数据库层拒绝第二行 → 代码层先 `SELECT ... WHERE tenant_id AND content_fingerprint` 命中即复用
|
||||
(`idempotent=true`),并发下再兜唯一冲突 → 回读既有行,实现并发安全幂等;
|
||||
- 跨租户相同内容**允许**各存一份(租户隔离优先于全局去重);
|
||||
- 指纹只覆盖业务内容,`created_at`/`id`/`created_by` 等易变字段已被 `strip_volatile` 排除,否则每次编译指纹都不同、幂等失效。
|
||||
- 表为 **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`(能力/组件注册表)
|
||||
| 字段 | 用途 |
|
||||
|---|---|
|
||||
| id / tenant_id | 主键 + 租户 |
|
||||
| cap_code | 能力编码(如 `role.skill`、`item.consume`) |
|
||||
| cap_kind | 能力类别(entity/rule/item/event/ui…) |
|
||||
| schema_json | 能力参数 schema(编译期做参数校验) |
|
||||
| version / state | 能力版本与启用状态(`enabled` 才参与编译) |
|
||||
| 字段 | 类型 | 用途 |
|
||||
|---|---|---|
|
||||
| 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(tenant_id, cap_code, version)`。编译时对**启用能力集合**做规范化后取 `sha256` 得 `registry_hash`,写入 GD 与版本表。
|
||||
约束:`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`(编译任务与审计载体)
|
||||
| 字段 | 用途 |
|
||||
|---|---|
|
||||
| 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 | 审计(不参与指纹) |
|
||||
### 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 | 审计(不参与指纹) |
|
||||
|
||||
索引:`(tenant_id, blueprint_id, created_at)`、`(tenant_id, state)`。
|
||||
约束/索引:`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`,三处同步)
|
||||
## 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/*.dspy`,**磁盘实测 `ls wwwroot/api/*.dspy | wc -l` = 15**,下表即磁盘清单
|
||||
(无虚构项、无遗漏项;实现函数名 = 文件名去后缀,全部列入 `api.py:__all__`):
|
||||
|
||||
| # | 端点(wwwroot/api/) | api.py 函数 | 方法 | 说明 |
|
||||
| # | 端点文件(`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 | 取版本记录 |
|
||||
| 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 | 两版本结构化差异 |
|
||||
| 14 | `pbl_compiler_compare.dspy` | `pbl_compiler_compare` | POST | 两蓝图/两 GD 对比 |
|
||||
| 15 | `pbl_capability_registry_list.dspy` | `pbl_capability_registry_list` | GET | 能力注册表查询(含 registry_hash) |
|
||||
| 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`) |
|
||||
|
||||
### 三处同步 + RBAC 自查命令(交付前必跑)
|
||||
前端入口:`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
|
||||
# ① 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 函数一一对应(应无输出)
|
||||
|
||||
# ① 磁盘端点数(期望 15)
|
||||
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)
|
||||
|
||||
# ② __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. 代码结构
|
||||
## 4. 代码结构(磁盘实测目录树,`find . -path ./.git -prune -o -type f -print`)
|
||||
|
||||
```
|
||||
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 # 蓝图 → GD(10 顶层键)
|
||||
│ ├── compiler.py # compile_blueprint ①-⑧ 主流程 + fail-closed
|
||||
│ └── versioning.py # 版本登记/取回/diff(M3b)
|
||||
├── models/ # 4 张表定义(四段式 JSON)
|
||||
│ ├── pbl_compiler_version.json
|
||||
├── 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
|
||||
├── wwwroot/api/*.dspy # 15 个契约端点
|
||||
├── 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 + register())
|
||||
│ └── test_m3a_selfcheck.py # M3a 自检(6 组断言,可独立运行)
|
||||
└── skill/SKILL.md # 模块技能文档(与 README 双向同步)
|
||||
│ ├── load_path.py # RBAC 路径注册(PATHS 15 条 + register())
|
||||
│ └── test_m3a_selfcheck.py # M3a 自检(6 组断言 G1-G6,可独立运行,见 §6)
|
||||
└── skill/
|
||||
└── SKILL.md # 模块技能文档(与 README 双向同步,见 §9)
|
||||
```
|
||||
|
||||
### `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 迭代顺序依赖。
|
||||
> **磁盘事实说明(防回归)**:本模块**不含** `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 一键安装
|
||||
### 5.1 安装(由宿主应用 `apps/pbls/build.sh` 承担;模块自身无 build.sh)
|
||||
```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)。
|
||||
# ① 安装模块包(模块仓库根目录)
|
||||
cd modules/pbl_compiler && pip install .
|
||||
|
||||
### 5.2 宿主应用挂载(`apps/{应用}/app/{应用}.py`)
|
||||
# ② 建表(幂等 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 pbl_compiler import load_pbl_compiler
|
||||
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):
|
||||
# 模块 → 库名映射,集中在应用层,模块内禁止硬编码
|
||||
return {'pbl_compiler': 'pbl', 'pbl_blueprint': 'pbl', 'pbl_common': 'pbl'}.get(m, m)
|
||||
# 模块 → 库名映射集中在应用层;模块内禁止硬编码 DBNAME
|
||||
return {'pbl_common': 'pbl', 'pbl_blueprint': 'pbl', 'pbl_validation': 'pbl',
|
||||
'pbl_compiler': '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) # 再挂本模块
|
||||
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 名**)
|
||||
### 5.3 库名获取(**禁硬编码 DB 名**,历史退回项 #4)
|
||||
```python
|
||||
# 正确:从宿主应用注入的 ServerEnv 取
|
||||
def _db(env):
|
||||
return env.get_module_dbname('pbl_compiler')
|
||||
# ✅ 正确(.py 模块文件内):从宿主注入的 ServerEnv 取
|
||||
dbname = ServerEnv().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'
|
||||
```
|
||||
# ✅ 正确(.dspy 内):get_module_dbname 是 ServerEnv 注入的全局,无需 import、无需 ServerEnv()
|
||||
dbname = get_module_dbname('pbl_compiler')
|
||||
|
||||
### 5.4 RBAC 注册
|
||||
`scripts/load_path.py` 中 `PATHS` 列出全部 15 个契约路径 + 角色(如 `pbl.designer` / `pbl.teacher` / `pbl.admin`),`register()` 逐条注册并返回统计。**格式串占位符个数必须与参数个数一致**(上一轮 `TypeError: not enough arguments for format string` 已修):
|
||||
# ❌ 错误(已整改,禁止回归):
|
||||
# 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(幂等)
|
||||
### 5.5 建表 DDL(幂等,完整见 `sql/pbl_compiler.sql`)
|
||||
```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
|
||||
);
|
||||
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. 自检运行方法
|
||||
## 6. 自检运行方法(`scripts/test_m3a_selfcheck.py`)
|
||||
|
||||
```bash
|
||||
cd modules/pbl_compiler && python3 scripts/test_m3a_selfcheck.py
|
||||
cd modules/pbl_compiler
|
||||
python3 scripts/test_m3a_selfcheck.py ; echo "exit=$?"
|
||||
```
|
||||
退出码 0 = 全部通过;非 0 = 有断言失败(`build.sh` 第 ⑤ 步会因此中断安装)。**6 组断言**:
|
||||
- 输出逐条 `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:声称的自检脚本必须真实可跑)。
|
||||
|
||||
| 组 | 断言 | 覆盖需求 |
|
||||
|---|---|---|
|
||||
| 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 语义正确 |
|
||||
### 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 |
|
||||
|
||||
自检脚本**不依赖数据库与宿主应用**(纯函数级),可在 CI 中直接跑;脚本内使用防御式导入,函数名缺失时明确报 `SELFCHECK_FAIL: missing <name>` 而非静默跳过。
|
||||
**本轮实测结果(磁盘实跑,非声称)**:`--- 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. 编译主流程 `compile_blueprint()` ①-⑧(fail-closed)
|
||||
## 7. 编译主流程
|
||||
|
||||
| 步 | 动作 | 失败处理(fail-closed) |
|
||||
### 7.1 `compile_blueprint(blueprint_id, version_no, compiler_version, triggered_by, **kw)` ①-⑧
|
||||
(实现见 `api.py`,函数 docstring 与下表一致;契约入口 `pbl_compiler_compile.dspy`)
|
||||
|
||||
| 步 | 动作 | 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` |
|
||||
| ① | **审批门禁**(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 日志 |
|
||||
|
||||
**fail-closed 原则**:③④⑤⑥ 任一不通过 → 无 GD 产出、任务终态可追溯(`error_code` + `error_message`)、审计留痕;**不存在**「先产出再补校验」的路径。`pbl_compiler_preview` 走 ①③⑥⑦ 的**只读子集**(不写 GD、不写版本),用于前端试编译。
|
||||
**返回**:`{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 项退回防回归条款)
|
||||
|
||||
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 字符」这类自我指涉文本按空壳造假退回。
|
||||
| # | 陷阱 | 正确做法 |
|
||||
|---|---|---|
|
||||
| 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`(已适配) |
|
||||
|
||||
### 本轮 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 实测矛盾」按造假直接退回)。
|
||||
### 本轮(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 的操作技能文档**(怎么调、参数、返回、铁律、陷阱),`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`(两侧端点名应一一对应,无单侧独有)。
|
||||
`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. 上下游依赖
|
||||
|
||||
### 上游(本模块依赖)
|
||||
| 依赖 | 用途 | 缺失影响 |
|
||||
|---|---|---|
|
||||
| `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`)、权限路径注册 | 数据访问与鉴权不可用 |
|
||||
### 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) | 读 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 指纹做匿名聚合口径对齐 |
|
||||
| `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` 作为运行时事件与状态写入的定义源 |
|
||||
|
||||
### 契约稳定性承诺
|
||||
GD 的 **10 个顶层键**与 `content_fingerprint` 算法是跨模块契约,属**破坏性变更**范围:任何改动必须升 `gd_version` / `compiler_version`、同步全部下游模块、并重跑 §6 自检 6 组断言。
|
||||
### 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` 声称的文件磁盘真实存在(禁幽灵文件)
|
||||
|
||||
@ -46,10 +46,10 @@
|
||||
},
|
||||
{
|
||||
"name": "content_fingerprint",
|
||||
"comment": "SHA-256 指纹",
|
||||
"comment": "SHA-256 指纹(64 位 hexdigest,唯一约束 uk_gd_fp 成员)",
|
||||
"null": false,
|
||||
"type": "str",
|
||||
"length": 32
|
||||
"length": 64
|
||||
},
|
||||
{
|
||||
"name": "definition_json",
|
||||
|
||||
@ -187,11 +187,16 @@ def registry_hash(capabilities):
|
||||
continue
|
||||
key = cap.get('capability_key') or cap.get('key') or cap.get('code') or ''
|
||||
schema = cap.get('args_schema_json') or cap.get('args_schema') or cap.get('schema') or ''
|
||||
# 口径统一(README §8 P1 / QC 退回项):既接受**原始注册表行**的
|
||||
# args_schema_json,也接受 gd_builder 已派生的 args_schema_hash。
|
||||
# 派生值本身就是 short_hash(args_schema_json, 16),故两种入参得到
|
||||
# **完全相同**的哈希 —— manifest.registryHash 与自检重算值必然相等。
|
||||
pre_hashed = str(cap.get('args_schema_hash') or '')
|
||||
items.append({
|
||||
'capability_key': str(key),
|
||||
'version_no': int(cap.get('version_no') or cap.get('version') or 1),
|
||||
'args_schema_hash': short_hash(schema if isinstance(schema, str)
|
||||
else canonical_json(schema), 16),
|
||||
'args_schema_hash': pre_hashed or short_hash(
|
||||
schema if isinstance(schema, str) else canonical_json(schema), 16),
|
||||
'permission_required': str(cap.get('permission_required') or ''),
|
||||
'is_enabled': bool(cap.get('is_enabled', True)),
|
||||
})
|
||||
|
||||
@ -300,8 +300,23 @@ def main():
|
||||
and gd['manifest'].get('deterministic') is True
|
||||
and gd['manifest'].get('llmUsed') is False,
|
||||
'manifest 声明 schema/deterministic=True/llmUsed=False(零 LLM)')
|
||||
_check('G4', gd['manifest'].get('registryHash') == f_reghash(_registry()),
|
||||
'manifest.registryHash == canonical.registry_hash(能力注册表)')
|
||||
# 口径统一(README §8 P1):gd_builder 以**补齐派生字段后的
|
||||
# capabilities.items** 计算 registry_hash,故自检主断言对同一口径重算;
|
||||
# 第二条断言验证「派生不丢语义」——对原始注册表行重算必须得到同值。
|
||||
caps_items = (gd.get('capabilities') or {}).get('items') or []
|
||||
_check('G4', gd['manifest'].get('registryHash') == f_reghash(caps_items),
|
||||
'manifest.registryHash == canonical.registry_hash(GD capabilities.items)(统一口径)')
|
||||
# 派生不丢语义:GD items 是「注册表行 ∪ 蓝图引用但未注册的能力」,
|
||||
# 故 items 可能多于原始注册表(多出的标 registered=false,供 M4b
|
||||
# fail-closed 告警)。此处断言**原始注册表每条能力都在 items 中且
|
||||
# version_no 一致**,即派生过程未丢失/未篡改注册表语义。
|
||||
raw = {str(r.get('capability_key')): int(r.get('version_no') or 1)
|
||||
for r in _registry()}
|
||||
got = {str(i.get('capability_key')): int(i.get('version_no') or 1)
|
||||
for i in caps_items}
|
||||
missing = sorted(k for k in raw if got.get(k) != raw[k])
|
||||
_check('G4', not missing,
|
||||
'原始注册表能力全部进入 GD items 且 version_no 一致(缺失/不一致=%s)' % missing)
|
||||
|
||||
# ---------------- G5 同输入重编译 N 次指纹全等(US-11 / F-CP-03) ----------------
|
||||
print('-- G5 确定性:同输入重编译指纹全等 --')
|
||||
|
||||
161
skill/SKILL.md
161
skill/SKILL.md
@ -1,27 +1,150 @@
|
||||
# pbl_compiler 模块技能(自动生成骨架 + 人工补充)
|
||||
---
|
||||
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 Compiler v1(确定性编译 + Game Definition,M3a/M3b)
|
||||
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`)。
|
||||
|
||||
## 挂载
|
||||
`from pbl_compiler.init import load_pbl_compiler` → `load_pbl_compiler()`(应用 app/pbls.py init() 中按序调用)
|
||||
```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` 门禁)。
|
||||
|
||||
## 数据表(3 张)
|
||||
- `pbl_compiler_version`:编译器版本(29.6 确定性)
|
||||
- `pbl_game_definition`:Game Definition(第9章 schema + 指纹)
|
||||
- `pbl_capability_registry`:Capability Registry(第11章缺口补齐)
|
||||
## 数据表(4 张,`models/*.json` 四段式 + `sql/pbl_compiler.sql`)
|
||||
所有表**首列 `tenant_id`**,所有 SQL 强制带 `tenant_id`;缺失租户上下文 fail-closed 抛错,无「默认租户」兜底。
|
||||
|
||||
## 契约接口(7 个,路径 `/pbl_compiler/api/<name>.dspy`)
|
||||
- `pbl_compiler_compile`
|
||||
- `pbl_compiler_preview`
|
||||
- `pbl_compiler_compare`
|
||||
- `pbl_compiler_version_list`
|
||||
- `pbl_compiler_version_save`
|
||||
- `pbl_capability_list`
|
||||
- `pbl_capability_register`
|
||||
| 表 | 用途 | 关键约束 |
|
||||
|---|---|---|
|
||||
| `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`(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)。
|
||||
|
||||
## 陷阱
|
||||
- 库名一律 `ServerEnv().get_module_dbname('pbl_compiler')`,禁止硬编码 DBNAME。
|
||||
- sqlor 只有 `C/U/D/R/I/sqlExe`;查询走 pbl_common.api 的 q_all/q_one(已适配)。
|
||||
- 所有读写强制带 `tenant_id`(pbl_common.api.tenant_id()),缺失即 fail-closed 报错。
|
||||
- 新增契约需同步三处:api.py 定义 + __init__.py 导出 + init.py env 注册 + scripts/load_path.py 路径。
|
||||
- **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)。
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
-- pbl_compiler 表 DDL(自动生成,与 apps/pbls/scripts/ddl/pbls_tables.sql 同源)
|
||||
CREATE TABLE IF NOT EXISTS `pbl_compiler_version` (
|
||||
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT 租户ID(强制打头),
|
||||
`tenant_id` VARCHAR(32) NOT NULL COMMENT 租户ID(强制打头),
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 主键,
|
||||
`code` VARCHAR(64) NOT NULL COMMENT "版本编码",
|
||||
`semver` VARCHAR(64) NOT NULL COMMENT "语义化版本",
|
||||
@ -12,15 +12,15 @@ CREATE TABLE IF NOT EXISTS `pbl_compiler_version` (
|
||||
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_cv_code` (`tenant_id`, `code`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="编译器版本(29.6 确定性)";
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT="编译器版本(29.6 确定性)";
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `pbl_game_definition` (
|
||||
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT 租户ID(强制打头),
|
||||
`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(32) NOT NULL COMMENT "SHA-256 指纹",
|
||||
`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",
|
||||
@ -33,10 +33,10 @@ CREATE TABLE IF NOT EXISTS `pbl_game_definition` (
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_gd_fp` (`tenant_id`, `content_fingerprint`),
|
||||
KEY `idx_gd_bp` (`tenant_id`, `blueprint_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="Game Definition(第9章 schema + 指纹)";
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT="Game Definition(第9章 schema + 指纹)";
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `pbl_capability_registry` (
|
||||
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT 租户ID(强制打头),
|
||||
`tenant_id` VARCHAR(32) NOT NULL COMMENT 租户ID(强制打头),
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 主键,
|
||||
`capability_key` VARCHAR(64) NOT NULL COMMENT "能力键",
|
||||
`category` VARCHAR(64) NOT NULL COMMENT "分类",
|
||||
@ -49,5 +49,30 @@ CREATE TABLE IF NOT EXISTS `pbl_capability_registry` (
|
||||
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_cap_key` (`tenant_id`, `capability_key`, `version_no`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="Capability Registry(第11章缺口补齐)";
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT="Capability Registry(第11章缺口补齐)";
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `pbl_compile_task` (
|
||||
`tenant_id` VARCHAR(64) NOT NULL COMMENT 租户ID(强制打头),
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 主键,
|
||||
`task_no` VARCHAR(64) NOT NULL COMMENT "任务流水号(确定性派生 CT+blueprint_id+version+seq)",
|
||||
`blueprint_id` BIGINT UNSIGNED NOT NULL COMMENT "蓝图ID",
|
||||
`blueprint_version` INT NOT NULL DEFAULT 0 COMMENT "蓝图版本(须已审批)",
|
||||
`compiler_version` VARCHAR(32) NOT NULL COMMENT "编译器版本号",
|
||||
`ruleset_version` VARCHAR(32) NOT NULL COMMENT "规则集版本",
|
||||
`status` VARCHAR(32) NOT NULL DEFAULT 'pending' COMMENT "appcodes:pbl_compile_status(pending/running/success/failed)",
|
||||
`approval_id` BIGINT UNSIGNED NULL COMMENT "关联审批记录(未审批拒绝 F-CP-01)",
|
||||
`game_def_id` BIGINT UNSIGNED NULL COMMENT "产出的 Game Definition ID(success 回填)",
|
||||
`content_fingerprint` VARCHAR(64) NULL COMMENT "产物 SHA-256 指纹(success 回填,US-11 比对锚点)",
|
||||
`error_code` VARCHAR(64) NULL COMMENT "失败错误码(PBL_E_*)",
|
||||
`error_msg` VARCHAR(2048) NULL COMMENT "失败原因(fail-closed 可追溯)",
|
||||
`triggered_by` VARCHAR(64) NULL COMMENT "触发人(审计)",
|
||||
`duration_ms` INT NOT NULL DEFAULT 0 COMMENT "耗时(易变,不参与指纹)",
|
||||
`started_at` DATETIME NULL COMMENT "开始时间",
|
||||
`finished_at` DATETIME NULL COMMENT "结束时间",
|
||||
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
|
||||
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
|
||||
PRIMARY KEY (`id`),
|
||||
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`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT="编译任务(approval_id 门禁,F-CP-01/US-10)";
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user