--- name: pbl_blueprint description: PBL 蓝图聚合根与子对象、版本、模板(M1a)。蓝图 CRUD/树/fork、7 类子对象泛化契约、版本 change_delta、模板实例化与离线兜底。所有读写 tenant_id 强制打头,缺失即 fail-closed 拒绝。 --- # pbl_blueprint 模块技能 ## 1. 模块定位 `pbl_blueprint` 是 PBL(Project-Based Learning)平台的**蓝图域聚合根模块**,负责: - 蓝图主表 `pbl_blueprint` 的 CRUD / 列表 / 树 / fork - 7 类子对象的**泛化契约**(一套 CRUD 覆盖 7 张表) - 版本快照 `pbl_blueprint_version`(含 `change_delta` 增量、diff、rollback) - 模板 `pbl_blueprint_template` / `pbl_blueprint_template_item`(实例化 + 离线兜底) 模块是 **Python 包**(无 app.py / 无端口 / 无 Dockerfile),通过 `load_pbl_blueprint(env)` 挂到应用 `apps/pbls`。 ## 2. 目录结构 ``` modules/pbl_blueprint/ ├── pyproject.toml ├── scripts/load_path.py # RBAC 路径注册(API/页面/表权限) └── pbl_blueprint/ # 包目录 = 模块名(禁 src/) ├── __init__.py # 导出 load_pbl_blueprint ├── init.py # 装配入口 + env 注册 + 契约字典 ├── db.py # 库名解析 / sqlor 获取 / 异常体系 / 建表 ├── blueprint_crud.py # 蓝图 CRUD + 树 + fork + 版本契约 ├── subobjects.py # 7 类子对象泛化 CRUD ├── templates.py # 模板 CRUD + 实例化 + 离线兜底 ├── api.py # HTTP API 层 + API_PATHS + HANDLERS ├── models/*.json # 表定义(四段式 summary/fields/indexes/codes) ├── json/*.json # CRUD 定义(根键 tblname + params.editable/browserfields) ├── sql/pbl_blueprint.core.sql # 核心 4 表 DDL ├── sql/pbl_blueprint.subobjects.sql # 子对象 7 表 DDL ├── init/data.json # 初始化数据(内置模板) └── wwwroot/index.ui # 前端页面 ``` ## 3. 数据表(11 张) 核心 4 张:`pbl_blueprint`、`pbl_blueprint_version`、`pbl_blueprint_template`、`pbl_blueprint_template_item` 子对象 7 张:`pbl_blueprint_task`、`pbl_blueprint_mission`、`pbl_blueprint_role`、 `pbl_blueprint_learning_goal`、`pbl_blueprint_evidence_spec`、`pbl_blueprint_artifact_spec`、 `pbl_blueprint_reflection_spec` 统一约定: - 主键 `id` = varchar(32),32 位无横线 UUID(`db.new_id()`) - `tenant_id` varchar(32) NOT NULL,**每个索引首列** - 软删除 `deleted` tinyint(0 正常 / 1 已删除) - 结构化字段用 `json` 类型(`spec` / `config` / `snapshot` / `change_delta` / `rule` / `permissions`) - 审计五件套 `created_by/created_at/updated_by/updated_at/deleted` ## 4. 子对象泛化契约(重点) 7 类子对象共用一套函数,`obj_type` 分派到表: | obj_type | 表 | 编码前缀 | 特有字段 | |---|---|---|---| | task | pbl_blueprint_task | TASK | parent_id | | mission | pbl_blueprint_mission | MISSION | task_id(必填), unlock_condition | | role | pbl_blueprint_role | ROLE | task_id, permissions | | learning_goal | pbl_blueprint_learning_goal | GOAL | task_id, dimension, weight | | evidence_spec | pbl_blueprint_evidence_spec | EVI | task_id, mission_id, learning_goal_id, evidence_type, collect_mode, rule, weight | | artifact_spec | pbl_blueprint_artifact_spec | ART | task_id, mission_id, artifact_type, accept_criteria, required, weight | | reflection_spec | pbl_blueprint_reflection_spec | REF | task_id, mission_id, trigger_point, reflection_type, rubric_ref, weight | ```python from pbl_blueprint import load_pbl_blueprint contract = load_pbl_blueprint(env) contract['create_subobject'](tenant_id, 'mission', {'blueprint_id': bp_id, 'task_id': t_id, 'name': '关卡1'}, env=env) contract['list_subobjects'](tenant_id, 'task', bp_id, env=env) contract['batch_upsert_subobjects'](tenant_id, 'task', bp_id, rows, env=env) # Designer Agent 整树写入 ``` ## 5. 蓝图契约 ```python create_blueprint(tenant_id, data, env, operator) # 自动生成 code PBL-{yyyyMMdd}-{seq4} update_blueprint(tenant_id, blueprint_id, data, ...) # 状态流转校验 STATUS_FLOW delete_blueprint(tenant_id, blueprint_id, ...) # 软删(连带子对象+版本) get_blueprint(tenant_id, blueprint_id, with_children) # with_children=True 附带 7 类子树 list_blueprints(tenant_id, filters, page, page_size) # 分页 + keyword/status/subject 过滤 get_blueprint_tree(tenant_id, blueprint_id) # {obj_type: [rows]} fork_blueprint(tenant_id, blueprint_id, new_name, target_tenant_id) # 跨租户复制,ID 重映射 ``` 状态机:`draft → validating → validated → compiling → compiled → published → archived`(非法流转抛 `PblValidationError`)。 ## 6. 版本契约 ```python save_version(tenant_id, blueprint_id, remark) # 存全量 snapshot + 计算 change_delta,主表 version_no 自增 list_versions(tenant_id, blueprint_id, page, page_size) diff_versions(tenant_id, blueprint_id, version_a, version_b) # {added, updated, removed} rollback_version(tenant_id, blueprint_id, version_no) # 先自动存当前版本再回滚 ``` `change_delta` 结构:`{"added":[{"obj_type","id","name"}], "updated":[{"obj_type","id","fields"}], "removed":[...]}` ## 7. 模板与离线兜底 ```python create_template(tenant_id, {'name':..., 'items':[{'obj_type':'task','ref_key':'T1','name':...}, ...]}) instantiate_template(tenant_id, template_id, name, overrides) # ref_key/parent_ref 解析为真实 ID,use_count 自增 build_offline_fallback_blueprint(tenant_id, name) # Agent/网络不可用时的最小可用蓝图(9 条子对象) ``` 模板条目用 `ref_key` / `parent_ref` 做**模板内相对引用**,实例化时按 `SUBOBJECT_TYPES` 顺序解析为真实 ID(父先于子)。 ## 8. tenant_id 强制打头(铁律) - 每个契约函数第一入参 `tenant_id`,内部 `db.require_tenant()` 校验 - 缺失/空/非字符串 → 抛 `PblTenantRequired`(code=`PBL_TENANT_REQUIRED`),**fail-closed 不放行** - 所有 where 条件由 `_where(tenant_id, extra)` 构造,`tenant_id=` 永远在最前 - API 层 `_tid(params)` 从入参或 `context.tenant_id` 取,取不到直接失败 - 平台内置模板 `tenant_id='platform'`,`list_templates(include_platform=True)` 才并入 ## 9. 异常码 | code | 含义 | |---|---| | PBL_TENANT_REQUIRED | 租户上下文缺失 | | PBL_VALIDATION_ERROR | 入参校验失败 | | PBL_NOT_FOUND / PBL_BLUEPRINT_NOT_FOUND / PBL_SUBOBJECT_NOT_FOUND / PBL_VERSION_NOT_FOUND / PBL_TEMPLATE_NOT_FOUND | 记录不存在 | | PBL_UNKNOWN_OBJ_TYPE | 未知子对象类型 | | PBL_DB_UNAVAILABLE | sqlor 不可用 | ## 10. 数据访问约束 - 库名:`ServerEnv().get_module_dbname('pbl_blueprint')`,**禁止硬编码 DBNAME** - SQL 只用 sqlor 标准 API:`sor.C / sor.U / sor.D / sor.R / sor.sqlExe`,禁编造 save/list/insert - 建表:`db.ensure_tables()` 幂等执行 `sql/*.sql`(`CREATE TABLE IF NOT EXISTS`) ## 11. API 路径(25 个,见 api.py API_PATHS) 前缀 `/pbl_blueprint/`,分四组:`blueprint/*`(7)、`version/*`(4)、`subobject/*`(7)、`template/*`(7)。 RBAC 注册:`python -m pbl_blueprint.scripts.load_path`(幂等,注册 API + 页面 + 表权限共 39 条路径)。 ## 12. 下游模块依赖 - `pbl_validation`(M2)读 `get_blueprint_tree` 做 14 维校验,回写 `quality_level` - `pbl_compiler`(M3)读蓝图树生成 Game Definition - `pbl_evidence`(M5)按 `evidence_spec` / `artifact_spec` 采集 - `pbl_assessment`(M6)按 `learning_goal.weight` / `reflection_spec.rubric_ref` 评分 - `pbl_agent_runtime`(M4)用 `batch_upsert_subobjects` 写 Designer 产出,fail-closed 裁决