diff --git a/README.md b/README.md index d8f761f..20530de 100644 --- a/README.md +++ b/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. = ` 注册; +④ `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 -u -p < sql/pbl_compiler.sql + +# ③ 前端链接:模块 wwwroot 软链到宿主应用 wwwroot(软链非 cp,保持同步) +ln -sf /modules/pbl_compiler/wwwroot /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 ` 并返回 `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 ` 并计入 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 ` 而非静默跳过。 +**本轮实测结果(磁盘实跑,非声称)**:`--- 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 '' 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, `);另 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` 声称的文件磁盘真实存在(禁幽灵文件) diff --git a/models/pbl_game_definition.json b/models/pbl_game_definition.json index 7b25087..b086a86 100644 --- a/models/pbl_game_definition.json +++ b/models/pbl_game_definition.json @@ -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", diff --git a/pbl_compiler/canonical.py b/pbl_compiler/canonical.py index dc0dfc6..e7b4f9b 100644 --- a/pbl_compiler/canonical.py +++ b/pbl_compiler/canonical.py @@ -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)), }) diff --git a/scripts/test_m3a_selfcheck.py b/scripts/test_m3a_selfcheck.py index f817c81..a0e2a49 100644 --- a/scripts/test_m3a_selfcheck.py +++ b/scripts/test_m3a_selfcheck.py @@ -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 确定性:同输入重编译指纹全等 --') diff --git a/skill/SKILL.md b/skill/SKILL.md index 8a8a474..e6996c7 100644 --- a/skill/SKILL.md +++ b/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/.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/.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 ` 属预期 fail-safe,判据是**不抛异常**。 +- **P6 三处同步漏一处** → `.dspy` 500 `NameError: name 'xxx' is not defined`。 +- **P7 volatile 混入指纹** → 每次编译指纹不同,`uk_gd_fp` 幂等失效;新增易变字段必须同步进 `VOLATILE_KEYS`。 +- **P8 文档描述磁盘不存在的代码文件**(`compiler.py`/`versioning.py`/模块 `build.sh`)。 +- **P9 指纹路径引入不确定性**(NaN、无 `ORDER BY`、dict 迭代序、`datetime.now()`)。 +- **P10 `return` 写在 `async with db.sqlorContext(...)` 内** → 静默 `None`(`return data type error, `); + 块内收集、**退出块后**再 `return`。sqlor 只有 `C/U/D/R/I/sqlExe`(`sor.I(ns)` 只 1 个参数); + 查询走 `pbl_common.api` 的 `q_all`/`q_one`(已适配)。 +- 其它:`.dspy` 内**禁 import**(全局已预载)、禁 `ServerEnv()` 取 per-request 用户(用 `request._run_ns`)、 + `py_compile` 对 `.dspy` 无效(用 grep 审计)、`trigger` 是 MySQL 关键字(本模块用 `triggered_by` 规避)、 + 手工建表必须 `COLLATE utf8mb4_unicode_ci`(否则 `Illegal mix of collations`)。 + +## 依赖 +**上游**:`pbl_common`(租户/DB 适配/错误码/审计/CRUD 工厂)、`pbl_blueprint`(蓝图 + 版本快照 + 审批)、 +`pbl_validation`(`quality_state` 门禁)、`pbl_appcodes`(枚举)、`world`/`scene`(GD 落库目标)、 +基础模块 `ahserver`/`sqlor`/`appbase`/`rbac`(只读,禁改)。 +**下游**:`pbl_agent_runtime`(M4)、`pbl_evidence`(M5)、`pbl_assessment`(M6)、`pbl_kdb_ext`(M7)、 +`pbl_scense_ext`/`scense`(M9)、`pbl_runtime_ext`(M11)。 +**开发顺序**:`pbl_common` → `pbl_appcodes` → `pbl_blueprint`(M1) → `pbl_validation`(M2) → +**`pbl_compiler`(M3)** → `pbl_agent_runtime`(M4) → `pbl_evidence`(M5) → `pbl_assessment`(M6) → +`pbl_kdb_ext`(M7) → `pbl_scense_ext`(M9) → `pbl_runtime_ext`(M11)。 diff --git a/sql/pbl_compiler.sql b/sql/pbl_compiler.sql index 31b671d..f439b02 100644 --- a/sql/pbl_compiler.sql +++ b/sql/pbl_compiler.sql @@ -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)";